pdd-cli 0.0.90__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 +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +506 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +537 -0
- pdd/agentic_common.py +533 -770
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +582 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +27 -9
- pdd/agentic_verify.py +3 -2
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +236 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +113 -48
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +358 -0
- pdd/commands/fix.py +155 -114
- pdd/commands/generate.py +5 -0
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +225 -163
- pdd/commands/sessions.py +284 -0
- pdd/commands/utility.py +12 -7
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +44 -7
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +68 -20
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +208 -6
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +531 -97
- pdd/load_prompt_template.py +15 -34
- pdd/operation_log.py +342 -0
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +122 -97
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +19 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1347 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +217 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +289 -211
- pdd/sync_order.py +304 -0
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +68 -26
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
- pdd_cli-0.0.121.dist-info/RECORD +229 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/sync_orchestration.py
CHANGED
|
@@ -27,6 +27,16 @@ MAX_CONSECUTIVE_CRASHES = 3 # Allow up to 3 consecutive crash attempts (Bug #15
|
|
|
27
27
|
|
|
28
28
|
# --- Real PDD Component Imports ---
|
|
29
29
|
from .sync_tui import SyncApp
|
|
30
|
+
from .operation_log import (
|
|
31
|
+
load_operation_log,
|
|
32
|
+
create_log_entry,
|
|
33
|
+
update_log_entry,
|
|
34
|
+
append_log_entry,
|
|
35
|
+
log_event,
|
|
36
|
+
save_fingerprint,
|
|
37
|
+
save_run_report,
|
|
38
|
+
clear_run_report,
|
|
39
|
+
)
|
|
30
40
|
from .sync_determine_operation import (
|
|
31
41
|
sync_determine_operation,
|
|
32
42
|
get_pdd_file_paths,
|
|
@@ -38,6 +48,7 @@ from .sync_determine_operation import (
|
|
|
38
48
|
read_run_report,
|
|
39
49
|
calculate_sha256,
|
|
40
50
|
calculate_current_hashes,
|
|
51
|
+
_safe_basename,
|
|
41
52
|
)
|
|
42
53
|
from .auto_deps_main import auto_deps_main
|
|
43
54
|
from .code_generator_main import code_generator_main
|
|
@@ -49,10 +60,14 @@ from .fix_main import fix_main
|
|
|
49
60
|
from .update_main import update_main
|
|
50
61
|
from .python_env_detector import detect_host_python_executable
|
|
51
62
|
from .get_run_command import get_run_command_for_file
|
|
52
|
-
from .pytest_output import extract_failing_files_from_output
|
|
63
|
+
from .pytest_output import extract_failing_files_from_output, _find_project_root
|
|
53
64
|
from . import DEFAULT_STRENGTH
|
|
54
65
|
|
|
55
66
|
|
|
67
|
+
# --- Helper Functions ---
|
|
68
|
+
# Note: _safe_basename is imported from sync_determine_operation
|
|
69
|
+
|
|
70
|
+
|
|
56
71
|
# --- Atomic State Update (Issue #159 Fix) ---
|
|
57
72
|
|
|
58
73
|
@dataclass
|
|
@@ -147,69 +162,11 @@ class AtomicStateUpdate:
|
|
|
147
162
|
self._temp_files.clear()
|
|
148
163
|
|
|
149
164
|
|
|
150
|
-
# ---
|
|
165
|
+
# --- State Management Wrappers ---
|
|
151
166
|
|
|
152
|
-
def
|
|
153
|
-
"""Load sync log entries for a basename and language."""
|
|
154
|
-
log_file = META_DIR / f"{basename}_{language}_sync.log"
|
|
155
|
-
if not log_file.exists():
|
|
156
|
-
return []
|
|
157
|
-
try:
|
|
158
|
-
with open(log_file, 'r') as f:
|
|
159
|
-
return [json.loads(line) for line in f if line.strip()]
|
|
160
|
-
except Exception:
|
|
161
|
-
return []
|
|
162
|
-
|
|
163
|
-
def create_sync_log_entry(decision, budget_remaining: float) -> Dict[str, Any]:
|
|
164
|
-
"""Create initial log entry from decision with all fields (actual results set to None initially)."""
|
|
165
|
-
return {
|
|
166
|
-
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
167
|
-
"operation": decision.operation,
|
|
168
|
-
"reason": decision.reason,
|
|
169
|
-
"decision_type": decision.details.get("decision_type", "heuristic") if decision.details else "heuristic",
|
|
170
|
-
"confidence": decision.confidence,
|
|
171
|
-
"estimated_cost": decision.estimated_cost,
|
|
172
|
-
"actual_cost": None,
|
|
173
|
-
"success": None,
|
|
174
|
-
"model": None,
|
|
175
|
-
"duration": None,
|
|
176
|
-
"error": None,
|
|
177
|
-
"details": {
|
|
178
|
-
**(decision.details if decision.details else {}),
|
|
179
|
-
"budget_remaining": budget_remaining
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
def update_sync_log_entry(entry: Dict[str, Any], result: Dict[str, Any], duration: float) -> Dict[str, Any]:
|
|
184
|
-
"""Update log entry with execution results (actual_cost, success, model, duration, error)."""
|
|
185
|
-
entry.update({
|
|
186
|
-
"actual_cost": result.get("cost", 0.0),
|
|
187
|
-
"success": result.get("success", False),
|
|
188
|
-
"model": result.get("model", "unknown"),
|
|
189
|
-
"duration": duration,
|
|
190
|
-
"error": result.get("error") if not result.get("success") else None
|
|
191
|
-
})
|
|
192
|
-
return entry
|
|
193
|
-
|
|
194
|
-
def append_sync_log(basename: str, language: str, entry: Dict[str, Any]):
|
|
195
|
-
"""Append completed log entry to the sync log file."""
|
|
196
|
-
log_file = META_DIR / f"{basename}_{language}_sync.log"
|
|
197
|
-
META_DIR.mkdir(parents=True, exist_ok=True)
|
|
198
|
-
with open(log_file, 'a') as f:
|
|
199
|
-
f.write(json.dumps(entry) + '\n')
|
|
200
|
-
|
|
201
|
-
def log_sync_event(basename: str, language: str, event: str, details: Dict[str, Any] = None):
|
|
202
|
-
"""Log a special sync event (lock_acquired, budget_warning, etc.)."""
|
|
203
|
-
entry = {
|
|
204
|
-
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
205
|
-
"event": event,
|
|
206
|
-
"details": details or {}
|
|
207
|
-
}
|
|
208
|
-
append_sync_log(basename, language, entry)
|
|
209
|
-
|
|
210
|
-
def save_run_report(report: Dict[str, Any], basename: str, language: str,
|
|
167
|
+
def _save_run_report_atomic(report: Dict[str, Any], basename: str, language: str,
|
|
211
168
|
atomic_state: Optional['AtomicStateUpdate'] = None):
|
|
212
|
-
"""Save a run report to the metadata directory.
|
|
169
|
+
"""Save a run report to the metadata directory, supporting atomic updates.
|
|
213
170
|
|
|
214
171
|
Args:
|
|
215
172
|
report: The run report dictionary to save.
|
|
@@ -217,20 +174,18 @@ def save_run_report(report: Dict[str, Any], basename: str, language: str,
|
|
|
217
174
|
language: The programming language.
|
|
218
175
|
atomic_state: Optional AtomicStateUpdate for atomic writes (Issue #159 fix).
|
|
219
176
|
"""
|
|
220
|
-
report_file = META_DIR / f"{basename}_{language}_run.json"
|
|
221
177
|
if atomic_state:
|
|
222
178
|
# Buffer for atomic write
|
|
179
|
+
report_file = META_DIR / f"{_safe_basename(basename)}_{language}_run.json"
|
|
223
180
|
atomic_state.set_run_report(report, report_file)
|
|
224
181
|
else:
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
with open(report_file, 'w') as f:
|
|
228
|
-
json.dump(report, f, indent=2, default=str)
|
|
182
|
+
# Direct write using operation_log
|
|
183
|
+
save_run_report(basename, language, report)
|
|
229
184
|
|
|
230
|
-
def
|
|
185
|
+
def _save_fingerprint_atomic(basename: str, language: str, operation: str,
|
|
231
186
|
paths: Dict[str, Path], cost: float, model: str,
|
|
232
187
|
atomic_state: Optional['AtomicStateUpdate'] = None):
|
|
233
|
-
"""Save fingerprint state after successful operation.
|
|
188
|
+
"""Save fingerprint state after successful operation, supporting atomic updates.
|
|
234
189
|
|
|
235
190
|
Args:
|
|
236
191
|
basename: The module basename.
|
|
@@ -241,31 +196,29 @@ def _save_operation_fingerprint(basename: str, language: str, operation: str,
|
|
|
241
196
|
model: The model used.
|
|
242
197
|
atomic_state: Optional AtomicStateUpdate for atomic writes (Issue #159 fix).
|
|
243
198
|
"""
|
|
244
|
-
from datetime import datetime, timezone
|
|
245
|
-
from .sync_determine_operation import calculate_current_hashes, Fingerprint
|
|
246
|
-
from . import __version__
|
|
247
|
-
|
|
248
|
-
current_hashes = calculate_current_hashes(paths)
|
|
249
|
-
fingerprint = Fingerprint(
|
|
250
|
-
pdd_version=__version__,
|
|
251
|
-
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
252
|
-
command=operation,
|
|
253
|
-
prompt_hash=current_hashes.get('prompt_hash'),
|
|
254
|
-
code_hash=current_hashes.get('code_hash'),
|
|
255
|
-
example_hash=current_hashes.get('example_hash'),
|
|
256
|
-
test_hash=current_hashes.get('test_hash'),
|
|
257
|
-
test_files=current_hashes.get('test_files'), # Bug #156
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
fingerprint_file = META_DIR / f"{basename}_{language}.json"
|
|
261
199
|
if atomic_state:
|
|
262
200
|
# Buffer for atomic write
|
|
201
|
+
from datetime import datetime, timezone
|
|
202
|
+
from .sync_determine_operation import calculate_current_hashes, Fingerprint
|
|
203
|
+
from . import __version__
|
|
204
|
+
|
|
205
|
+
current_hashes = calculate_current_hashes(paths)
|
|
206
|
+
fingerprint = Fingerprint(
|
|
207
|
+
pdd_version=__version__,
|
|
208
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
209
|
+
command=operation,
|
|
210
|
+
prompt_hash=current_hashes.get('prompt_hash'),
|
|
211
|
+
code_hash=current_hashes.get('code_hash'),
|
|
212
|
+
example_hash=current_hashes.get('example_hash'),
|
|
213
|
+
test_hash=current_hashes.get('test_hash'),
|
|
214
|
+
test_files=current_hashes.get('test_files'), # Bug #156
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
fingerprint_file = META_DIR / f"{_safe_basename(basename)}_{language}.json"
|
|
263
218
|
atomic_state.set_fingerprint(asdict(fingerprint), fingerprint_file)
|
|
264
219
|
else:
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
with open(fingerprint_file, 'w') as f:
|
|
268
|
-
json.dump(asdict(fingerprint), f, indent=2, default=str)
|
|
220
|
+
# Direct write using operation_log
|
|
221
|
+
save_fingerprint(basename, language, operation, paths, cost, model)
|
|
269
222
|
|
|
270
223
|
def _python_cov_target_for_code_file(code_file: Path) -> str:
|
|
271
224
|
"""Return a `pytest-cov` `--cov` target for a Python code file.
|
|
@@ -574,7 +527,7 @@ def _try_auto_fix_import_error(
|
|
|
574
527
|
def _run_example_with_error_detection(
|
|
575
528
|
cmd_parts: list[str],
|
|
576
529
|
env: dict,
|
|
577
|
-
cwd: str,
|
|
530
|
+
cwd: Optional[str] = None,
|
|
578
531
|
timeout: int = 60
|
|
579
532
|
) -> tuple[int, str, str]:
|
|
580
533
|
"""
|
|
@@ -635,24 +588,23 @@ def _run_example_with_error_detection(
|
|
|
635
588
|
# Check for errors in output
|
|
636
589
|
has_errors, error_summary = _detect_example_errors(combined)
|
|
637
590
|
|
|
638
|
-
# Determine result:
|
|
639
|
-
# -
|
|
591
|
+
# Determine result (check returncode first, then use error detection for signal-killed):
|
|
592
|
+
# - Zero exit code → success (trust the exit code)
|
|
640
593
|
# - Positive exit code (process failed normally, e.g., sys.exit(1)) → failure
|
|
641
594
|
# - Negative exit code (killed by signal, e.g., -9 for SIGKILL) → check output
|
|
642
|
-
# - Zero exit code → success
|
|
643
595
|
#
|
|
644
596
|
# IMPORTANT: When we kill the process after timeout, returncode is negative
|
|
645
597
|
# (the signal number). This is NOT a failure if output has no errors.
|
|
646
|
-
if
|
|
647
|
-
return
|
|
598
|
+
if proc.returncode is not None and proc.returncode == 0:
|
|
599
|
+
return 0, stdout, stderr # Clean exit = success (trust exit code)
|
|
648
600
|
elif proc.returncode is not None and proc.returncode > 0:
|
|
649
601
|
return proc.returncode, stdout, stderr # Process exited with error
|
|
650
602
|
else:
|
|
651
|
-
#
|
|
652
|
-
# -
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return 0, stdout, stderr
|
|
603
|
+
# Killed by signal (returncode < 0 or None) - use error detection
|
|
604
|
+
# Server-style examples may run until timeout, need to check output
|
|
605
|
+
if has_errors:
|
|
606
|
+
return 1, stdout, stderr # Errors detected in output
|
|
607
|
+
return 0, stdout, stderr # No errors, server was running fine
|
|
656
608
|
|
|
657
609
|
|
|
658
610
|
def _execute_tests_and_create_run_report(
|
|
@@ -713,6 +665,10 @@ def _execute_tests_and_create_run_report(
|
|
|
713
665
|
if not cov_target:
|
|
714
666
|
cov_target = basename or module_name
|
|
715
667
|
|
|
668
|
+
# Find project root for proper pytest configuration (Bug fix: infinite fix loop)
|
|
669
|
+
# This matches the logic in pytest_output.py to ensure consistent behavior
|
|
670
|
+
project_root = _find_project_root(test_file)
|
|
671
|
+
|
|
716
672
|
# Bug #156: Run pytest on ALL test files
|
|
717
673
|
pytest_args = [
|
|
718
674
|
python_executable, '-m', 'pytest',
|
|
@@ -722,10 +678,37 @@ def _execute_tests_and_create_run_report(
|
|
|
722
678
|
f'--cov={cov_target}',
|
|
723
679
|
'--cov-report=term-missing'
|
|
724
680
|
]
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
681
|
+
|
|
682
|
+
# Set up project root configuration to prevent parent config interference
|
|
683
|
+
subprocess_cwd = None
|
|
684
|
+
if project_root is not None:
|
|
685
|
+
# Add PYTHONPATH to include project root and src/ directory
|
|
686
|
+
paths_to_add = [str(project_root)]
|
|
687
|
+
src_dir = project_root / "src"
|
|
688
|
+
if src_dir.is_dir():
|
|
689
|
+
paths_to_add.insert(0, str(src_dir))
|
|
690
|
+
existing_pythonpath = clean_env.get("PYTHONPATH", "")
|
|
691
|
+
if existing_pythonpath:
|
|
692
|
+
paths_to_add.append(existing_pythonpath)
|
|
693
|
+
clean_env["PYTHONPATH"] = os.pathsep.join(paths_to_add)
|
|
694
|
+
|
|
695
|
+
# Add --rootdir and -c /dev/null to prevent parent config discovery
|
|
696
|
+
pytest_args.extend([f'--rootdir={project_root}', '-c', '/dev/null'])
|
|
697
|
+
subprocess_cwd = str(project_root)
|
|
698
|
+
|
|
699
|
+
# Build subprocess kwargs - only include cwd if project root was found
|
|
700
|
+
subprocess_kwargs = {
|
|
701
|
+
'capture_output': True,
|
|
702
|
+
'text': True,
|
|
703
|
+
'timeout': 300,
|
|
704
|
+
'stdin': subprocess.DEVNULL,
|
|
705
|
+
'env': clean_env,
|
|
706
|
+
'start_new_session': True,
|
|
707
|
+
}
|
|
708
|
+
if subprocess_cwd is not None:
|
|
709
|
+
subprocess_kwargs['cwd'] = subprocess_cwd
|
|
710
|
+
|
|
711
|
+
result = subprocess.run(pytest_args, **subprocess_kwargs)
|
|
729
712
|
|
|
730
713
|
exit_code = result.returncode
|
|
731
714
|
stdout = result.stdout + (result.stderr or '')
|
|
@@ -746,7 +729,7 @@ def _execute_tests_and_create_run_report(
|
|
|
746
729
|
test_hash=test_hash,
|
|
747
730
|
test_files=test_file_hashes, # Bug #156
|
|
748
731
|
)
|
|
749
|
-
|
|
732
|
+
_save_run_report_atomic(asdict(report), basename, language, atomic_state)
|
|
750
733
|
return report
|
|
751
734
|
|
|
752
735
|
# Run the test command
|
|
@@ -789,7 +772,7 @@ def _execute_tests_and_create_run_report(
|
|
|
789
772
|
test_files=test_file_hashes, # Bug #156
|
|
790
773
|
)
|
|
791
774
|
|
|
792
|
-
|
|
775
|
+
_save_run_report_atomic(asdict(report), basename, language, atomic_state)
|
|
793
776
|
return report
|
|
794
777
|
|
|
795
778
|
def _create_mock_context(**kwargs) -> click.Context:
|
|
@@ -801,12 +784,12 @@ def _create_mock_context(**kwargs) -> click.Context:
|
|
|
801
784
|
|
|
802
785
|
def _display_sync_log(basename: str, language: str, verbose: bool = False) -> Dict[str, Any]:
|
|
803
786
|
"""Displays the sync log for a given basename and language."""
|
|
804
|
-
log_file = META_DIR / f"{basename}_{language}_sync.log"
|
|
787
|
+
log_file = META_DIR / f"{_safe_basename(basename)}_{language}_sync.log"
|
|
805
788
|
if not log_file.exists():
|
|
806
789
|
print(f"No sync log found for '{basename}' in language '{language}'.")
|
|
807
790
|
return {'success': False, 'errors': ['Log file not found.'], 'log_entries': []}
|
|
808
791
|
|
|
809
|
-
log_entries =
|
|
792
|
+
log_entries = load_operation_log(basename, language)
|
|
810
793
|
print(f"--- Sync Log for {basename} ({language}) ---")
|
|
811
794
|
|
|
812
795
|
if not log_entries:
|
|
@@ -904,6 +887,14 @@ def sync_orchestration(
|
|
|
904
887
|
"""
|
|
905
888
|
Orchestrates the complete PDD sync workflow with parallel animation.
|
|
906
889
|
"""
|
|
890
|
+
# Handle None values from CLI (Issue #194) - defense in depth
|
|
891
|
+
if target_coverage is None:
|
|
892
|
+
target_coverage = 90.0
|
|
893
|
+
if budget is None:
|
|
894
|
+
budget = 10.0
|
|
895
|
+
if max_attempts is None:
|
|
896
|
+
max_attempts = 3
|
|
897
|
+
|
|
907
898
|
# Import get_extension at function scope
|
|
908
899
|
from .sync_determine_operation import get_extension
|
|
909
900
|
|
|
@@ -967,6 +958,10 @@ def sync_orchestration(
|
|
|
967
958
|
"""Get the confirmation callback from the app if available.
|
|
968
959
|
|
|
969
960
|
Once user confirms, we remember it so subsequent operations don't ask again.
|
|
961
|
+
|
|
962
|
+
Fix for Issue #277: In headless mode, we now return a wrapper callback
|
|
963
|
+
that uses click.confirm AND sets user_confirmed_overwrite[0] = True,
|
|
964
|
+
so subsequent calls auto-confirm instead of prompting repeatedly.
|
|
970
965
|
"""
|
|
971
966
|
if user_confirmed_overwrite[0]:
|
|
972
967
|
# User already confirmed, return a callback that always returns True
|
|
@@ -979,6 +974,26 @@ def sync_orchestration(
|
|
|
979
974
|
user_confirmed_overwrite[0] = True
|
|
980
975
|
return result
|
|
981
976
|
return confirming_callback
|
|
977
|
+
|
|
978
|
+
# Fix #277: In headless mode (app_ref is None), create a wrapper callback
|
|
979
|
+
# that sets the flag after confirmation, preventing repeated prompts
|
|
980
|
+
if confirm_callback is None:
|
|
981
|
+
def headless_confirming_callback(msg: str, title: str) -> bool:
|
|
982
|
+
"""Headless mode callback that remembers user confirmation."""
|
|
983
|
+
try:
|
|
984
|
+
prompt = msg or "Overwrite existing files?"
|
|
985
|
+
result = click.confirm(
|
|
986
|
+
click.style(prompt, fg="yellow"),
|
|
987
|
+
default=True,
|
|
988
|
+
show_default=True
|
|
989
|
+
)
|
|
990
|
+
except (click.Abort, EOFError):
|
|
991
|
+
return False
|
|
992
|
+
if result:
|
|
993
|
+
user_confirmed_overwrite[0] = True
|
|
994
|
+
return result
|
|
995
|
+
return headless_confirming_callback
|
|
996
|
+
|
|
982
997
|
return confirm_callback # Fall back to provided callback
|
|
983
998
|
|
|
984
999
|
def sync_worker_logic():
|
|
@@ -998,28 +1013,39 @@ def sync_orchestration(
|
|
|
998
1013
|
|
|
999
1014
|
try:
|
|
1000
1015
|
with SyncLock(basename, language):
|
|
1001
|
-
|
|
1016
|
+
log_event(basename, language, "lock_acquired", {"pid": os.getpid()}, invocation_mode="sync")
|
|
1002
1017
|
|
|
1003
1018
|
while True:
|
|
1004
1019
|
budget_remaining = budget - current_cost_ref[0]
|
|
1005
1020
|
if current_cost_ref[0] >= budget:
|
|
1006
1021
|
errors.append(f"Budget of ${budget:.2f} exceeded.")
|
|
1007
|
-
|
|
1022
|
+
log_event(basename, language, "budget_exceeded", {
|
|
1008
1023
|
"total_cost": current_cost_ref[0],
|
|
1009
1024
|
"budget": budget
|
|
1010
|
-
})
|
|
1025
|
+
}, invocation_mode="sync")
|
|
1011
1026
|
break
|
|
1012
1027
|
|
|
1013
1028
|
if budget_remaining < budget * 0.2 and budget_remaining > 0:
|
|
1014
|
-
|
|
1029
|
+
log_event(basename, language, "budget_warning", {
|
|
1015
1030
|
"remaining": budget_remaining,
|
|
1016
1031
|
"percentage": (budget_remaining / budget) * 100
|
|
1017
|
-
})
|
|
1032
|
+
}, invocation_mode="sync")
|
|
1018
1033
|
|
|
1019
1034
|
decision = sync_determine_operation(basename, language, target_coverage, budget_remaining, False, prompts_dir, skip_tests, skip_verify, context_override)
|
|
1020
1035
|
operation = decision.operation
|
|
1021
1036
|
|
|
1022
|
-
log_entry =
|
|
1037
|
+
log_entry = create_log_entry(
|
|
1038
|
+
operation=decision.operation,
|
|
1039
|
+
reason=decision.reason,
|
|
1040
|
+
invocation_mode="sync",
|
|
1041
|
+
estimated_cost=decision.estimated_cost,
|
|
1042
|
+
confidence=decision.confidence,
|
|
1043
|
+
decision_type=decision.details.get("decision_type", "heuristic") if decision.details else "heuristic"
|
|
1044
|
+
)
|
|
1045
|
+
if decision.details:
|
|
1046
|
+
log_entry.setdefault('details', {}).update(decision.details)
|
|
1047
|
+
log_entry.setdefault('details', {})['budget_remaining'] = budget_remaining
|
|
1048
|
+
|
|
1023
1049
|
operation_history.append(operation)
|
|
1024
1050
|
|
|
1025
1051
|
# Cycle detection logic
|
|
@@ -1027,7 +1053,7 @@ def sync_orchestration(
|
|
|
1027
1053
|
recent_auto_deps = [op for op in operation_history[-3:] if op == 'auto-deps']
|
|
1028
1054
|
if len(recent_auto_deps) >= 2:
|
|
1029
1055
|
errors.append("Detected auto-deps infinite loop. Force advancing to generate operation.")
|
|
1030
|
-
|
|
1056
|
+
log_event(basename, language, "cycle_detected", {"cycle_type": "auto-deps-infinite"}, invocation_mode="sync")
|
|
1031
1057
|
operation = 'generate'
|
|
1032
1058
|
decision.operation = 'generate' # Update decision too
|
|
1033
1059
|
|
|
@@ -1040,7 +1066,7 @@ def sync_orchestration(
|
|
|
1040
1066
|
recent_ops == ['verify', 'crash', 'verify', 'crash']):
|
|
1041
1067
|
# Pattern detected - this represents MAX_CYCLE_REPEATS iterations
|
|
1042
1068
|
errors.append(f"Detected crash-verify cycle repeated {MAX_CYCLE_REPEATS} times. Breaking cycle.")
|
|
1043
|
-
|
|
1069
|
+
log_event(basename, language, "cycle_detected", {"cycle_type": "crash-verify", "count": MAX_CYCLE_REPEATS}, invocation_mode="sync")
|
|
1044
1070
|
break
|
|
1045
1071
|
|
|
1046
1072
|
# Bug #4 fix: Detect test-fix cycle pattern
|
|
@@ -1052,7 +1078,7 @@ def sync_orchestration(
|
|
|
1052
1078
|
recent_ops == ['fix', 'test', 'fix', 'test']):
|
|
1053
1079
|
# Pattern detected - this represents MAX_CYCLE_REPEATS iterations
|
|
1054
1080
|
errors.append(f"Detected test-fix cycle repeated {MAX_CYCLE_REPEATS} times. Breaking cycle.")
|
|
1055
|
-
|
|
1081
|
+
log_event(basename, language, "cycle_detected", {"cycle_type": "test-fix", "count": MAX_CYCLE_REPEATS}, invocation_mode="sync")
|
|
1056
1082
|
break
|
|
1057
1083
|
|
|
1058
1084
|
if operation == 'fix':
|
|
@@ -1094,11 +1120,11 @@ def sync_orchestration(
|
|
|
1094
1120
|
extend_attempts = sum(1 for op in operation_history if op == 'test_extend')
|
|
1095
1121
|
if extend_attempts >= MAX_TEST_EXTEND_ATTEMPTS:
|
|
1096
1122
|
# Accept current coverage after max attempts
|
|
1097
|
-
|
|
1123
|
+
log_event(basename, language, "test_extend_limit", {
|
|
1098
1124
|
"attempts": extend_attempts,
|
|
1099
1125
|
"max_attempts": MAX_TEST_EXTEND_ATTEMPTS,
|
|
1100
1126
|
"reason": "Accepting current coverage after max extend attempts"
|
|
1101
|
-
})
|
|
1127
|
+
}, invocation_mode="sync")
|
|
1102
1128
|
success = True
|
|
1103
1129
|
break
|
|
1104
1130
|
|
|
@@ -1116,32 +1142,32 @@ def sync_orchestration(
|
|
|
1116
1142
|
errors.append(f"Conflict detected: {decision.reason}")
|
|
1117
1143
|
error_msg = decision.reason
|
|
1118
1144
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1145
|
+
update_log_entry(log_entry, success=success, cost=0.0, model='none', duration=0.0, error=error_msg)
|
|
1146
|
+
append_log_entry(basename, language, log_entry)
|
|
1121
1147
|
break
|
|
1122
1148
|
|
|
1123
1149
|
# Handle skips - save fingerprint with 'skip:' prefix to distinguish from actual execution
|
|
1124
1150
|
# Bug #11 fix: Use 'skip:' prefix so _is_workflow_complete() knows the op was skipped
|
|
1125
1151
|
if operation == 'verify' and (skip_verify or skip_tests):
|
|
1126
1152
|
skipped_operations.append('verify')
|
|
1127
|
-
|
|
1128
|
-
|
|
1153
|
+
update_log_entry(log_entry, success=True, cost=0.0, model='skipped', duration=0.0, error=None)
|
|
1154
|
+
append_log_entry(basename, language, log_entry)
|
|
1129
1155
|
# Save fingerprint with 'skip:' prefix to indicate operation was skipped, not executed
|
|
1130
|
-
|
|
1156
|
+
_save_fingerprint_atomic(basename, language, 'skip:verify', pdd_files, 0.0, 'skipped')
|
|
1131
1157
|
continue
|
|
1132
1158
|
if operation == 'test' and skip_tests:
|
|
1133
1159
|
skipped_operations.append('test')
|
|
1134
|
-
|
|
1135
|
-
|
|
1160
|
+
update_log_entry(log_entry, success=True, cost=0.0, model='skipped', duration=0.0, error=None)
|
|
1161
|
+
append_log_entry(basename, language, log_entry)
|
|
1136
1162
|
# Save fingerprint with 'skip:' prefix to indicate operation was skipped, not executed
|
|
1137
|
-
|
|
1163
|
+
_save_fingerprint_atomic(basename, language, 'skip:test', pdd_files, 0.0, 'skipped')
|
|
1138
1164
|
continue
|
|
1139
1165
|
if operation == 'crash' and (skip_tests or skip_verify):
|
|
1140
1166
|
skipped_operations.append('crash')
|
|
1141
|
-
|
|
1142
|
-
|
|
1167
|
+
update_log_entry(log_entry, success=True, cost=0.0, model='skipped', duration=0.0, error=None)
|
|
1168
|
+
append_log_entry(basename, language, log_entry)
|
|
1143
1169
|
# Save fingerprint with 'skip:' prefix to indicate operation was skipped, not executed
|
|
1144
|
-
|
|
1170
|
+
_save_fingerprint_atomic(basename, language, 'skip:crash', pdd_files, 0.0, 'skipped')
|
|
1145
1171
|
# FIX: Create a synthetic run_report to prevent infinite loop when crash is skipped
|
|
1146
1172
|
# Without this, sync_determine_operation keeps returning 'crash' because no run_report exists
|
|
1147
1173
|
current_hashes = calculate_current_hashes(pdd_files)
|
|
@@ -1153,7 +1179,7 @@ def sync_orchestration(
|
|
|
1153
1179
|
coverage=0.0,
|
|
1154
1180
|
test_hash=current_hashes.get('test_hash')
|
|
1155
1181
|
)
|
|
1156
|
-
|
|
1182
|
+
_save_run_report_atomic(asdict(synthetic_report), basename, language)
|
|
1157
1183
|
continue
|
|
1158
1184
|
|
|
1159
1185
|
current_function_name_ref[0] = operation
|
|
@@ -1196,12 +1222,17 @@ def sync_orchestration(
|
|
|
1196
1222
|
Path(temp_output).unlink()
|
|
1197
1223
|
result = (new_content, 0.0, 'no-changes')
|
|
1198
1224
|
elif operation == 'generate':
|
|
1199
|
-
|
|
1225
|
+
# Ensure code directory exists before generating
|
|
1226
|
+
pdd_files['code'].parent.mkdir(parents=True, exist_ok=True)
|
|
1227
|
+
# Use absolute paths to avoid path_resolution_mode mismatch between sync (cwd) and generate (config_base)
|
|
1228
|
+
result = code_generator_main(ctx, prompt_file=str(pdd_files['prompt'].resolve()), output=str(pdd_files['code'].resolve()), original_prompt_file_path=None, force_incremental_flag=False)
|
|
1200
1229
|
# Clear stale run_report so crash/verify is required for newly generated code
|
|
1201
|
-
|
|
1202
|
-
run_report_file.unlink(missing_ok=True)
|
|
1230
|
+
clear_run_report(basename, language)
|
|
1203
1231
|
elif operation == 'example':
|
|
1204
|
-
|
|
1232
|
+
# Ensure example directory exists before generating
|
|
1233
|
+
pdd_files['example'].parent.mkdir(parents=True, exist_ok=True)
|
|
1234
|
+
# Use absolute paths to avoid path_resolution_mode mismatch between sync (cwd) and example (config_base)
|
|
1235
|
+
result = context_generator_main(ctx, prompt_file=str(pdd_files['prompt'].resolve()), code_file=str(pdd_files['code'].resolve()), output=str(pdd_files['example'].resolve()))
|
|
1205
1236
|
elif operation == 'crash':
|
|
1206
1237
|
required_files = [pdd_files['code'], pdd_files['example']]
|
|
1207
1238
|
missing_files = [f for f in required_files if not f.exists()]
|
|
@@ -1221,25 +1252,20 @@ def sync_orchestration(
|
|
|
1221
1252
|
else:
|
|
1222
1253
|
# Manual check - run the example to see if it crashes
|
|
1223
1254
|
env = os.environ.copy()
|
|
1224
|
-
|
|
1225
|
-
env['PYTHONPATH'] = f"{
|
|
1255
|
+
code_dir = pdd_files['code'].resolve().parent
|
|
1256
|
+
env['PYTHONPATH'] = f"{code_dir}:{env.get('PYTHONPATH', '')}"
|
|
1226
1257
|
# Remove TUI-specific env vars that might contaminate subprocess
|
|
1227
1258
|
for var in ['FORCE_COLOR', 'COLUMNS']:
|
|
1228
1259
|
env.pop(var, None)
|
|
1229
|
-
#
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
cmd_parts = run_cmd.split()
|
|
1235
|
-
else:
|
|
1236
|
-
# Fallback to Python if no run command found
|
|
1237
|
-
cmd_parts = ['python', example_path]
|
|
1260
|
+
# Bug fix: Use sys.executable to match crash_main's Python interpreter
|
|
1261
|
+
# and do NOT set cwd - inherit from pdd invocation directory
|
|
1262
|
+
# to match crash_main behavior. Setting cwd to example's parent breaks imports.
|
|
1263
|
+
example_path = str(pdd_files['example'].resolve())
|
|
1264
|
+
cmd_parts = [sys.executable, example_path]
|
|
1238
1265
|
# Use error-detection runner that handles server-style examples
|
|
1239
1266
|
returncode, stdout, stderr = _run_example_with_error_detection(
|
|
1240
1267
|
cmd_parts,
|
|
1241
1268
|
env=env,
|
|
1242
|
-
cwd=str(pdd_files['example'].parent),
|
|
1243
1269
|
timeout=60
|
|
1244
1270
|
)
|
|
1245
1271
|
|
|
@@ -1268,7 +1294,7 @@ def sync_orchestration(
|
|
|
1268
1294
|
coverage=0.0,
|
|
1269
1295
|
test_hash=test_hash
|
|
1270
1296
|
)
|
|
1271
|
-
|
|
1297
|
+
_save_run_report_atomic(asdict(report), basename, language)
|
|
1272
1298
|
skipped_operations.append('crash')
|
|
1273
1299
|
continue
|
|
1274
1300
|
|
|
@@ -1280,17 +1306,16 @@ def sync_orchestration(
|
|
|
1280
1306
|
pdd_files['example']
|
|
1281
1307
|
)
|
|
1282
1308
|
if auto_fixed:
|
|
1283
|
-
|
|
1309
|
+
log_event(basename, language, "auto_fix_attempted", {"message": auto_fix_msg}, invocation_mode="sync")
|
|
1284
1310
|
# Retry running the example after auto-fix
|
|
1285
1311
|
retry_returncode, retry_stdout, retry_stderr = _run_example_with_error_detection(
|
|
1286
1312
|
cmd_parts,
|
|
1287
1313
|
env=env,
|
|
1288
|
-
cwd=str(pdd_files['example'].parent),
|
|
1289
1314
|
timeout=60
|
|
1290
1315
|
)
|
|
1291
1316
|
if retry_returncode == 0:
|
|
1292
1317
|
# Auto-fix worked! Save run report and continue
|
|
1293
|
-
|
|
1318
|
+
log_event(basename, language, "auto_fix_success", {"message": auto_fix_msg}, invocation_mode="sync")
|
|
1294
1319
|
test_hash = calculate_sha256(pdd_files['test']) if pdd_files['test'].exists() else None
|
|
1295
1320
|
report = RunReport(
|
|
1296
1321
|
datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
@@ -1300,7 +1325,7 @@ def sync_orchestration(
|
|
|
1300
1325
|
coverage=0.0,
|
|
1301
1326
|
test_hash=test_hash
|
|
1302
1327
|
)
|
|
1303
|
-
|
|
1328
|
+
_save_run_report_atomic(asdict(report), basename, language)
|
|
1304
1329
|
result = (True, 0.0, 'auto-fix')
|
|
1305
1330
|
success = True
|
|
1306
1331
|
actual_cost = 0.0
|
|
@@ -1314,7 +1339,10 @@ def sync_orchestration(
|
|
|
1314
1339
|
|
|
1315
1340
|
Path("crash.log").write_text(crash_log_content)
|
|
1316
1341
|
try:
|
|
1317
|
-
|
|
1342
|
+
# For non-Python languages, set max_attempts=0 to skip iterative loop
|
|
1343
|
+
# and go directly to agentic fallback
|
|
1344
|
+
effective_max_attempts = 0 if language.lower() != 'python' else max_attempts
|
|
1345
|
+
result = crash_main(ctx, prompt_file=str(pdd_files['prompt']), code_file=str(pdd_files['code']), program_file=str(pdd_files['example']), error_file="crash.log", output=str(pdd_files['code']), output_program=str(pdd_files['example']), loop=True, max_attempts=effective_max_attempts, budget=budget - current_cost_ref[0], strength=strength, temperature=temperature)
|
|
1318
1346
|
except Exception as e:
|
|
1319
1347
|
print(f"Crash fix failed: {e}")
|
|
1320
1348
|
skipped_operations.append('crash')
|
|
@@ -1324,7 +1352,10 @@ def sync_orchestration(
|
|
|
1324
1352
|
if not pdd_files['example'].exists():
|
|
1325
1353
|
skipped_operations.append('verify')
|
|
1326
1354
|
continue
|
|
1327
|
-
|
|
1355
|
+
# For non-Python languages, set max_attempts=0 to skip iterative loop
|
|
1356
|
+
# and go directly to agentic fallback
|
|
1357
|
+
effective_max_attempts = 0 if language.lower() != 'python' else max_attempts
|
|
1358
|
+
result = fix_verification_main(ctx, prompt_file=str(pdd_files['prompt']), code_file=str(pdd_files['code']), program_file=str(pdd_files['example']), output_results=f"{basename.replace('/', '_')}_verify_results.log", output_code=str(pdd_files['code']), output_program=str(pdd_files['example']), loop=True, verification_program=str(pdd_files['example']), max_attempts=effective_max_attempts, budget=budget - current_cost_ref[0], strength=strength, temperature=temperature)
|
|
1328
1359
|
elif operation == 'test':
|
|
1329
1360
|
pdd_files['test'].parent.mkdir(parents=True, exist_ok=True)
|
|
1330
1361
|
# Use merge=True when test file exists to preserve fixes and append new tests
|
|
@@ -1402,12 +1433,37 @@ def sync_orchestration(
|
|
|
1402
1433
|
# Bug #156: Run pytest on ALL matching test files
|
|
1403
1434
|
test_files = pdd_files.get('test_files', [pdd_files['test']])
|
|
1404
1435
|
pytest_args = [python_executable, '-m', 'pytest'] + [str(f) for f in test_files] + ['-v', '--tb=short']
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1436
|
+
|
|
1437
|
+
# Bug fix: Find project root for proper pytest configuration
|
|
1438
|
+
# This matches the fix in _execute_tests_and_create_run_report()
|
|
1439
|
+
project_root = _find_project_root(pdd_files['test'])
|
|
1440
|
+
|
|
1441
|
+
# Set up subprocess kwargs
|
|
1442
|
+
subprocess_kwargs = {
|
|
1443
|
+
'capture_output': True,
|
|
1444
|
+
'text': True,
|
|
1445
|
+
'timeout': 300,
|
|
1446
|
+
'stdin': subprocess.DEVNULL,
|
|
1447
|
+
'env': clean_env,
|
|
1448
|
+
'start_new_session': True
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if project_root is not None:
|
|
1452
|
+
# Add PYTHONPATH to include project root and src/ directory
|
|
1453
|
+
paths_to_add = [str(project_root)]
|
|
1454
|
+
src_dir = project_root / "src"
|
|
1455
|
+
if src_dir.is_dir():
|
|
1456
|
+
paths_to_add.insert(0, str(src_dir))
|
|
1457
|
+
existing_pythonpath = clean_env.get("PYTHONPATH", "")
|
|
1458
|
+
if existing_pythonpath:
|
|
1459
|
+
paths_to_add.append(existing_pythonpath)
|
|
1460
|
+
clean_env["PYTHONPATH"] = os.pathsep.join(paths_to_add)
|
|
1461
|
+
|
|
1462
|
+
# Add --rootdir and -c /dev/null to prevent parent config discovery
|
|
1463
|
+
pytest_args.extend([f'--rootdir={project_root}', '-c', '/dev/null'])
|
|
1464
|
+
subprocess_kwargs['cwd'] = str(project_root)
|
|
1465
|
+
|
|
1466
|
+
test_result = subprocess.run(pytest_args, **subprocess_kwargs)
|
|
1411
1467
|
else:
|
|
1412
1468
|
# Use shell command for non-Python
|
|
1413
1469
|
test_result = subprocess.run(
|
|
@@ -1460,7 +1516,10 @@ def sync_orchestration(
|
|
|
1460
1516
|
unit_test_file_for_fix = str(ff_path.resolve())
|
|
1461
1517
|
break
|
|
1462
1518
|
|
|
1463
|
-
|
|
1519
|
+
# For non-Python languages, set max_attempts=0 to skip iterative loop
|
|
1520
|
+
# and go directly to agentic fallback
|
|
1521
|
+
effective_max_attempts = 0 if language.lower() != 'python' else max_attempts
|
|
1522
|
+
result = fix_main(ctx, prompt_file=str(pdd_files['prompt']), code_file=str(pdd_files['code']), unit_test_file=unit_test_file_for_fix, error_file=str(error_file_path), output_test=str(pdd_files['test']), output_code=str(pdd_files['code']), output_results=f"{basename.replace('/', '_')}_fix_results.log", loop=True, verification_program=str(pdd_files['example']), max_attempts=effective_max_attempts, budget=budget - current_cost_ref[0], auto_submit=True, strength=strength, temperature=temperature)
|
|
1464
1523
|
elif operation == 'update':
|
|
1465
1524
|
result = update_main(ctx, input_prompt_file=str(pdd_files['prompt']), modified_code_file=str(pdd_files['code']), input_code_file=None, output=str(pdd_files['prompt']), use_git=True, strength=strength, temperature=temperature)
|
|
1466
1525
|
else:
|
|
@@ -1479,8 +1538,12 @@ def sync_orchestration(
|
|
|
1479
1538
|
else:
|
|
1480
1539
|
success = result is not None
|
|
1481
1540
|
|
|
1541
|
+
except click.Abort:
|
|
1542
|
+
errors.append(f"Operation '{operation}' was cancelled (user declined or non-interactive environment)")
|
|
1543
|
+
success = False
|
|
1482
1544
|
except Exception as e:
|
|
1483
|
-
|
|
1545
|
+
error_msg = str(e) if str(e) else type(e).__name__
|
|
1546
|
+
errors.append(f"Exception during '{operation}': {error_msg}")
|
|
1484
1547
|
success = False
|
|
1485
1548
|
|
|
1486
1549
|
# Log update
|
|
@@ -1496,10 +1559,10 @@ def sync_orchestration(
|
|
|
1496
1559
|
model_name = result[-1] if len(result) >= 1 else 'unknown'
|
|
1497
1560
|
last_model_name = str(model_name)
|
|
1498
1561
|
operations_completed.append(operation)
|
|
1499
|
-
|
|
1562
|
+
_save_fingerprint_atomic(basename, language, operation, pdd_files, actual_cost, str(model_name), atomic_state=atomic_state)
|
|
1500
1563
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1564
|
+
update_log_entry(log_entry, success=success, cost=actual_cost, model=model_name, duration=duration, error=errors[-1] if errors and not success else None)
|
|
1565
|
+
append_log_entry(basename, language, log_entry)
|
|
1503
1566
|
|
|
1504
1567
|
# Post-operation checks (simplified)
|
|
1505
1568
|
if success and operation == 'crash':
|
|
@@ -1509,29 +1572,29 @@ def sync_orchestration(
|
|
|
1509
1572
|
clean_env = os.environ.copy()
|
|
1510
1573
|
for var in ['FORCE_COLOR', 'COLUMNS']:
|
|
1511
1574
|
clean_env.pop(var, None)
|
|
1512
|
-
#
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1575
|
+
# Bug fix: Use sys.executable to ensure same Python interpreter as
|
|
1576
|
+
# crash_main (fix_code_loop.py:477). When both venv and conda are
|
|
1577
|
+
# active, PATH lookup for 'python' may resolve to a different
|
|
1578
|
+
# interpreter, causing infinite crash loops.
|
|
1579
|
+
# Bug fix: Do NOT set cwd - inherit from pdd invocation directory
|
|
1580
|
+
# to match crash_main behavior. Setting cwd to example's parent breaks imports.
|
|
1581
|
+
example_path = str(pdd_files['example'].resolve())
|
|
1582
|
+
cmd_parts = [sys.executable, example_path]
|
|
1519
1583
|
# Use error-detection runner that handles server-style examples
|
|
1520
1584
|
returncode, stdout, stderr = _run_example_with_error_detection(
|
|
1521
1585
|
cmd_parts,
|
|
1522
1586
|
env=clean_env,
|
|
1523
|
-
cwd=str(pdd_files['example'].parent),
|
|
1524
1587
|
timeout=60
|
|
1525
1588
|
)
|
|
1526
1589
|
# Include test_hash for staleness detection
|
|
1527
1590
|
test_hash = calculate_sha256(pdd_files['test']) if pdd_files['test'].exists() else None
|
|
1528
1591
|
report = RunReport(datetime.datetime.now(datetime.timezone.utc).isoformat(), returncode, 1 if returncode==0 else 0, 0 if returncode==0 else 1, 100.0 if returncode==0 else 0.0, test_hash=test_hash)
|
|
1529
|
-
|
|
1592
|
+
_save_run_report_atomic(asdict(report), basename, language)
|
|
1530
1593
|
except Exception as e:
|
|
1531
1594
|
# Bug #8 fix: Don't silently swallow exceptions - log them and mark as error
|
|
1532
1595
|
error_msg = f"Post-crash verification failed: {e}"
|
|
1533
1596
|
errors.append(error_msg)
|
|
1534
|
-
|
|
1597
|
+
log_event(basename, language, "post_crash_verification_failed", {"error": str(e)}, invocation_mode="sync")
|
|
1535
1598
|
|
|
1536
1599
|
if success and operation == 'fix':
|
|
1537
1600
|
# Re-run tests to update run_report after successful fix
|
|
@@ -1548,7 +1611,8 @@ def sync_orchestration(
|
|
|
1548
1611
|
)
|
|
1549
1612
|
|
|
1550
1613
|
if not success:
|
|
1551
|
-
|
|
1614
|
+
if not errors:
|
|
1615
|
+
errors.append(f"Operation '{operation}' failed.")
|
|
1552
1616
|
break
|
|
1553
1617
|
|
|
1554
1618
|
except BaseException as e:
|
|
@@ -1558,7 +1622,7 @@ def sync_orchestration(
|
|
|
1558
1622
|
traceback.print_exc()
|
|
1559
1623
|
finally:
|
|
1560
1624
|
try:
|
|
1561
|
-
|
|
1625
|
+
log_event(basename, language, "lock_released", {"pid": os.getpid(), "total_cost": current_cost_ref[0]}, invocation_mode="sync")
|
|
1562
1626
|
except: pass
|
|
1563
1627
|
|
|
1564
1628
|
# Return result dict
|
|
@@ -1574,48 +1638,62 @@ def sync_orchestration(
|
|
|
1574
1638
|
'model_name': last_model_name,
|
|
1575
1639
|
}
|
|
1576
1640
|
|
|
1577
|
-
#
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1641
|
+
# Detect headless mode (no TTY, CI environment, or quiet mode)
|
|
1642
|
+
headless = quiet or not sys.stdout.isatty() or os.environ.get('CI')
|
|
1643
|
+
|
|
1644
|
+
if headless:
|
|
1645
|
+
# Set PDD_FORCE to also skip API key prompts in headless mode
|
|
1646
|
+
os.environ['PDD_FORCE'] = '1'
|
|
1647
|
+
# Run worker logic directly without TUI in headless mode
|
|
1648
|
+
if not quiet:
|
|
1649
|
+
print(f"Running sync in headless mode (CI/non-TTY environment)...")
|
|
1650
|
+
result = sync_worker_logic()
|
|
1651
|
+
# No TUI app, so no worker_exception to check
|
|
1652
|
+
worker_exception = None
|
|
1653
|
+
else:
|
|
1654
|
+
# Instantiate and run Textual App
|
|
1655
|
+
app = SyncApp(
|
|
1656
|
+
basename=basename,
|
|
1657
|
+
budget=budget,
|
|
1658
|
+
worker_func=sync_worker_logic,
|
|
1659
|
+
function_name_ref=current_function_name_ref,
|
|
1660
|
+
cost_ref=current_cost_ref,
|
|
1661
|
+
prompt_path_ref=prompt_path_ref,
|
|
1662
|
+
code_path_ref=code_path_ref,
|
|
1663
|
+
example_path_ref=example_path_ref,
|
|
1664
|
+
tests_path_ref=tests_path_ref,
|
|
1665
|
+
prompt_color_ref=prompt_box_color_ref,
|
|
1666
|
+
code_color_ref=code_box_color_ref,
|
|
1667
|
+
example_color_ref=example_box_color_ref,
|
|
1668
|
+
tests_color_ref=tests_box_color_ref,
|
|
1669
|
+
stop_event=stop_event,
|
|
1670
|
+
progress_callback_ref=progress_callback_ref
|
|
1671
|
+
)
|
|
1595
1672
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1673
|
+
# Store app reference so worker can access request_confirmation
|
|
1674
|
+
app_ref[0] = app
|
|
1598
1675
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if not quiet:
|
|
1676
|
+
result = app.run()
|
|
1677
|
+
|
|
1678
|
+
# Show exit animation if not quiet
|
|
1603
1679
|
from .sync_tui import show_exit_animation
|
|
1604
1680
|
show_exit_animation()
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1681
|
+
|
|
1682
|
+
worker_exception = app.worker_exception
|
|
1683
|
+
|
|
1684
|
+
# Check for worker exception that might have caused a crash (TUI mode only)
|
|
1685
|
+
if not headless and worker_exception:
|
|
1686
|
+
print(f"\n[Error] Worker thread crashed with exception: {worker_exception}", file=sys.stderr)
|
|
1687
|
+
|
|
1610
1688
|
if hasattr(app, 'captured_logs') and app.captured_logs:
|
|
1611
1689
|
print("\n[Captured Logs (last 20 lines)]", file=sys.stderr)
|
|
1612
1690
|
for line in app.captured_logs[-20:]: # Print last 20 lines
|
|
1613
1691
|
print(f" {line}", file=sys.stderr)
|
|
1614
|
-
|
|
1692
|
+
|
|
1615
1693
|
import traceback
|
|
1616
1694
|
# Use trace module to print the stored exception's traceback if available
|
|
1617
|
-
if hasattr(
|
|
1618
|
-
traceback.print_exception(type(
|
|
1695
|
+
if hasattr(worker_exception, '__traceback__'):
|
|
1696
|
+
traceback.print_exception(type(worker_exception), worker_exception, worker_exception.__traceback__, file=sys.stderr)
|
|
1619
1697
|
|
|
1620
1698
|
if result is None:
|
|
1621
1699
|
return {
|
|
@@ -1639,4 +1717,4 @@ if __name__ == '__main__':
|
|
|
1639
1717
|
PDD_DIR.mkdir(exist_ok=True)
|
|
1640
1718
|
META_DIR.mkdir(exist_ok=True)
|
|
1641
1719
|
result = sync_orchestration(basename="my_calculator", language="python", quiet=True)
|
|
1642
|
-
print(json.dumps(result, indent=2))
|
|
1720
|
+
print(json.dumps(result, indent=2))
|