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.
Files changed (151) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +506 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +537 -0
  6. pdd/agentic_common.py +533 -770
  7. pdd/agentic_crash.py +2 -1
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +582 -0
  10. pdd/agentic_fix.py +118 -3
  11. pdd/agentic_update.py +27 -9
  12. pdd/agentic_verify.py +3 -2
  13. pdd/architecture_sync.py +565 -0
  14. pdd/auth_service.py +210 -0
  15. pdd/auto_deps_main.py +63 -53
  16. pdd/auto_include.py +236 -3
  17. pdd/auto_update.py +125 -47
  18. pdd/bug_main.py +195 -23
  19. pdd/cmd_test_main.py +345 -197
  20. pdd/code_generator.py +4 -2
  21. pdd/code_generator_main.py +118 -32
  22. pdd/commands/__init__.py +6 -0
  23. pdd/commands/analysis.py +113 -48
  24. pdd/commands/auth.py +309 -0
  25. pdd/commands/connect.py +358 -0
  26. pdd/commands/fix.py +155 -114
  27. pdd/commands/generate.py +5 -0
  28. pdd/commands/maintenance.py +3 -2
  29. pdd/commands/misc.py +8 -0
  30. pdd/commands/modify.py +225 -163
  31. pdd/commands/sessions.py +284 -0
  32. pdd/commands/utility.py +12 -7
  33. pdd/construct_paths.py +334 -32
  34. pdd/context_generator_main.py +167 -170
  35. pdd/continue_generation.py +6 -3
  36. pdd/core/__init__.py +33 -0
  37. pdd/core/cli.py +44 -7
  38. pdd/core/cloud.py +237 -0
  39. pdd/core/dump.py +68 -20
  40. pdd/core/errors.py +4 -0
  41. pdd/core/remote_session.py +61 -0
  42. pdd/crash_main.py +219 -23
  43. pdd/data/llm_model.csv +4 -4
  44. pdd/docs/prompting_guide.md +864 -0
  45. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  46. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  47. pdd/fix_code_loop.py +208 -34
  48. pdd/fix_code_module_errors.py +6 -2
  49. pdd/fix_error_loop.py +291 -38
  50. pdd/fix_main.py +208 -6
  51. pdd/fix_verification_errors_loop.py +235 -26
  52. pdd/fix_verification_main.py +269 -83
  53. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  54. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  55. pdd/frontend/dist/index.html +376 -0
  56. pdd/frontend/dist/logo.svg +33 -0
  57. pdd/generate_output_paths.py +46 -5
  58. pdd/generate_test.py +212 -151
  59. pdd/get_comment.py +19 -44
  60. pdd/get_extension.py +8 -9
  61. pdd/get_jwt_token.py +309 -20
  62. pdd/get_language.py +8 -7
  63. pdd/get_run_command.py +7 -5
  64. pdd/insert_includes.py +2 -1
  65. pdd/llm_invoke.py +531 -97
  66. pdd/load_prompt_template.py +15 -34
  67. pdd/operation_log.py +342 -0
  68. pdd/path_resolution.py +140 -0
  69. pdd/postprocess.py +122 -97
  70. pdd/preprocess.py +68 -12
  71. pdd/preprocess_main.py +33 -1
  72. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  73. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  74. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  75. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  76. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  77. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  78. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  79. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  80. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  81. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  82. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  83. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  84. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
  85. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  86. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  87. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  88. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  89. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  90. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  91. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  92. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  93. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  94. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  95. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  96. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  97. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  98. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  99. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  100. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  101. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  102. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  103. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  104. pdd/prompts/agentic_update_LLM.prompt +192 -338
  105. pdd/prompts/auto_include_LLM.prompt +22 -0
  106. pdd/prompts/change_LLM.prompt +3093 -1
  107. pdd/prompts/detect_change_LLM.prompt +571 -14
  108. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  109. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  110. pdd/prompts/generate_test_LLM.prompt +19 -1
  111. pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
  112. pdd/prompts/insert_includes_LLM.prompt +262 -252
  113. pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
  114. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  115. pdd/remote_session.py +876 -0
  116. pdd/server/__init__.py +52 -0
  117. pdd/server/app.py +335 -0
  118. pdd/server/click_executor.py +587 -0
  119. pdd/server/executor.py +338 -0
  120. pdd/server/jobs.py +661 -0
  121. pdd/server/models.py +241 -0
  122. pdd/server/routes/__init__.py +31 -0
  123. pdd/server/routes/architecture.py +451 -0
  124. pdd/server/routes/auth.py +364 -0
  125. pdd/server/routes/commands.py +929 -0
  126. pdd/server/routes/config.py +42 -0
  127. pdd/server/routes/files.py +603 -0
  128. pdd/server/routes/prompts.py +1347 -0
  129. pdd/server/routes/websocket.py +473 -0
  130. pdd/server/security.py +243 -0
  131. pdd/server/terminal_spawner.py +217 -0
  132. pdd/server/token_counter.py +222 -0
  133. pdd/summarize_directory.py +236 -237
  134. pdd/sync_animation.py +8 -4
  135. pdd/sync_determine_operation.py +329 -47
  136. pdd/sync_main.py +272 -28
  137. pdd/sync_orchestration.py +289 -211
  138. pdd/sync_order.py +304 -0
  139. pdd/template_expander.py +161 -0
  140. pdd/templates/architecture/architecture_json.prompt +41 -46
  141. pdd/trace.py +1 -1
  142. pdd/track_cost.py +0 -13
  143. pdd/unfinished_prompt.py +2 -1
  144. pdd/update_main.py +68 -26
  145. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
  146. pdd_cli-0.0.121.dist-info/RECORD +229 -0
  147. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  148. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  149. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  150. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  151. {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
- # --- Mock Helper Functions ---
165
+ # --- State Management Wrappers ---
151
166
 
152
- def load_sync_log(basename: str, language: str) -> List[Dict[str, Any]]:
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
- # Legacy direct write
226
- META_DIR.mkdir(parents=True, exist_ok=True)
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 _save_operation_fingerprint(basename: str, language: str, operation: str,
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
- # Legacy direct write
266
- META_DIR.mkdir(parents=True, exist_ok=True)
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
- # - Errors in outputfailure
591
+ # Determine result (check returncode first, then use error detection for signal-killed):
592
+ # - Zero exit codesuccess (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 has_errors:
647
- return 1, stdout, stderr # Errors detected in output
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
- # Success cases:
652
- # - returncode == 0 (clean exit)
653
- # - returncode < 0 (killed by signal, but no errors in output)
654
- # - returncode is None (shouldn't happen after wait, but safe fallback)
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
- result = subprocess.run(
726
- pytest_args,
727
- capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, env=clean_env, start_new_session=True
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
- save_run_report(asdict(report), basename, language, atomic_state)
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
- save_run_report(asdict(report), basename, language, atomic_state)
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 = load_sync_log(basename, language)
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
- log_sync_event(basename, language, "lock_acquired", {"pid": os.getpid()})
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
- log_sync_event(basename, language, "budget_exceeded", {
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
- log_sync_event(basename, language, "budget_warning", {
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 = create_sync_log_entry(decision, budget_remaining)
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
- log_sync_event(basename, language, "cycle_detected", {"cycle_type": "auto-deps-infinite"})
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
- log_sync_event(basename, language, "cycle_detected", {"cycle_type": "crash-verify", "count": MAX_CYCLE_REPEATS})
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
- log_sync_event(basename, language, "cycle_detected", {"cycle_type": "test-fix", "count": MAX_CYCLE_REPEATS})
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
- log_sync_event(basename, language, "test_extend_limit", {
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
- update_sync_log_entry(log_entry, {'success': success, 'cost': 0.0, 'model': 'none', 'error': error_msg}, 0.0)
1120
- append_sync_log(basename, language, log_entry)
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
- update_sync_log_entry(log_entry, {'success': True, 'cost': 0.0, 'model': 'skipped', 'error': None}, 0.0)
1128
- append_sync_log(basename, language, log_entry)
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
- _save_operation_fingerprint(basename, language, 'skip:verify', pdd_files, 0.0, 'skipped')
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
- update_sync_log_entry(log_entry, {'success': True, 'cost': 0.0, 'model': 'skipped', 'error': None}, 0.0)
1135
- append_sync_log(basename, language, log_entry)
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
- _save_operation_fingerprint(basename, language, 'skip:test', pdd_files, 0.0, 'skipped')
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
- update_sync_log_entry(log_entry, {'success': True, 'cost': 0.0, 'model': 'skipped', 'error': None}, 0.0)
1142
- append_sync_log(basename, language, log_entry)
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
- _save_operation_fingerprint(basename, language, 'skip:crash', pdd_files, 0.0, 'skipped')
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
- save_run_report(asdict(synthetic_report), basename, language)
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
- result = code_generator_main(ctx, prompt_file=str(pdd_files['prompt']), output=str(pdd_files['code']), original_prompt_file_path=None, force_incremental_flag=False)
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
- run_report_file = META_DIR / f"{basename}_{language}_run.json"
1202
- run_report_file.unlink(missing_ok=True)
1230
+ clear_run_report(basename, language)
1203
1231
  elif operation == 'example':
1204
- result = context_generator_main(ctx, prompt_file=str(pdd_files['prompt']), code_file=str(pdd_files['code']), output=str(pdd_files['example']))
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
- src_dir = Path.cwd() / 'src'
1225
- env['PYTHONPATH'] = f"{src_dir}:{env.get('PYTHONPATH', '')}"
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
- # Get language-appropriate run command from language_format.csv
1230
- example_path = str(pdd_files['example'])
1231
- run_cmd = get_run_command_for_file(example_path)
1232
- if run_cmd:
1233
- # Use the language-specific interpreter (e.g., node for .js)
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
- save_run_report(asdict(report), basename, language)
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
- log_sync_event(basename, language, "auto_fix_attempted", {"message": auto_fix_msg})
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
- log_sync_event(basename, language, "auto_fix_success", {"message": auto_fix_msg})
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
- save_run_report(asdict(report), basename, language)
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
- 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=max_attempts, budget=budget - current_cost_ref[0], strength=strength, temperature=temperature)
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
- 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}_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=max_attempts, budget=budget - current_cost_ref[0], strength=strength, temperature=temperature)
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
- test_result = subprocess.run(
1406
- pytest_args,
1407
- capture_output=True, text=True, timeout=300,
1408
- stdin=subprocess.DEVNULL, env=clean_env, start_new_session=True,
1409
- cwd=str(pdd_files['test'].parent)
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
- 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}_fix_results.log", loop=True, verification_program=str(pdd_files['example']), max_attempts=max_attempts, budget=budget - current_cost_ref[0], auto_submit=True, strength=strength, temperature=temperature)
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
- errors.append(f"Exception during '{operation}': {e}")
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
- _save_operation_fingerprint(basename, language, operation, pdd_files, actual_cost, str(model_name), atomic_state=atomic_state)
1562
+ _save_fingerprint_atomic(basename, language, operation, pdd_files, actual_cost, str(model_name), atomic_state=atomic_state)
1500
1563
 
1501
- update_sync_log_entry(log_entry, {'success': success, 'cost': actual_cost, 'model': model_name, 'error': errors[-1] if errors and not success else None}, duration)
1502
- append_sync_log(basename, language, log_entry)
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
- # Get language-appropriate run command
1513
- example_path = str(pdd_files['example'])
1514
- run_cmd = get_run_command_for_file(example_path)
1515
- if run_cmd:
1516
- cmd_parts = run_cmd.split()
1517
- else:
1518
- cmd_parts = ['python', example_path]
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
- save_run_report(asdict(report), basename, language)
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
- log_sync_event(basename, language, "post_crash_verification_failed", {"error": str(e)})
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
- errors.append(f"Operation '{operation}' failed.")
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
- log_sync_event(basename, language, "lock_released", {"pid": os.getpid(), "total_cost": current_cost_ref[0]})
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
- # Instantiate and run Textual App
1578
- app = SyncApp(
1579
- basename=basename,
1580
- budget=budget,
1581
- worker_func=sync_worker_logic,
1582
- function_name_ref=current_function_name_ref,
1583
- cost_ref=current_cost_ref,
1584
- prompt_path_ref=prompt_path_ref,
1585
- code_path_ref=code_path_ref,
1586
- example_path_ref=example_path_ref,
1587
- tests_path_ref=tests_path_ref,
1588
- prompt_color_ref=prompt_box_color_ref,
1589
- code_color_ref=code_box_color_ref,
1590
- example_color_ref=example_box_color_ref,
1591
- tests_color_ref=tests_box_color_ref,
1592
- stop_event=stop_event,
1593
- progress_callback_ref=progress_callback_ref
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
- # Store app reference so worker can access request_confirmation
1597
- app_ref[0] = app
1673
+ # Store app reference so worker can access request_confirmation
1674
+ app_ref[0] = app
1598
1675
 
1599
- result = app.run()
1600
-
1601
- # Show exit animation if not quiet
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
- # Check for worker exception that might have caused a crash
1607
- if app.worker_exception:
1608
- print(f"\n[Error] Worker thread crashed with exception: {app.worker_exception}", file=sys.stderr)
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(app.worker_exception, '__traceback__'):
1618
- traceback.print_exception(type(app.worker_exception), app.worker_exception, app.worker_exception.__traceback__, file=sys.stderr)
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))