pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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 (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/construct_paths.py CHANGED
@@ -73,6 +73,146 @@ def list_available_contexts(start_path: Optional[Path] = None) -> list[str]:
73
73
  names = sorted(contexts.keys()) if isinstance(contexts, dict) else []
74
74
  return names or ["default"]
75
75
 
76
+ def _match_path_to_contexts(
77
+ path_str: str,
78
+ contexts: Dict[str, Any],
79
+ use_specificity: bool = False,
80
+ is_absolute: bool = False
81
+ ) -> Optional[str]:
82
+ """
83
+ Core pattern matching logic - matches a path against context patterns.
84
+
85
+ Args:
86
+ path_str: Path to match (can be relative or absolute)
87
+ contexts: The contexts dict from .pddrc config
88
+ use_specificity: If True, return most specific match; else first match
89
+ is_absolute: If True, use absolute path matching with "*/" prefix
90
+
91
+ Returns:
92
+ Context name or None
93
+ """
94
+ matches = []
95
+ for context_name, context_config in contexts.items():
96
+ if context_name == 'default':
97
+ continue
98
+ for path_pattern in context_config.get('paths', []):
99
+ pattern_base = path_pattern.rstrip('/**').rstrip('/*')
100
+
101
+ # Check for match - handle both absolute and relative paths
102
+ matched = False
103
+ if is_absolute:
104
+ # For absolute paths (CWD-based detection), use existing logic
105
+ if fnmatch.fnmatch(path_str, f"*/{path_pattern}") or \
106
+ fnmatch.fnmatch(path_str, path_pattern) or \
107
+ path_str.endswith(f"/{pattern_base}"):
108
+ matched = True
109
+ else:
110
+ # For relative paths (file-based detection)
111
+ if fnmatch.fnmatch(path_str, path_pattern) or \
112
+ path_str.startswith(pattern_base + '/') or \
113
+ path_str.startswith(pattern_base):
114
+ matched = True
115
+
116
+ if matched:
117
+ if not use_specificity:
118
+ return context_name # First match wins
119
+ matches.append((context_name, len(pattern_base)))
120
+
121
+ if matches:
122
+ matches.sort(key=lambda x: x[1], reverse=True)
123
+ return matches[0][0]
124
+
125
+ return 'default' if 'default' in contexts else None
126
+
127
+
128
+ def _detect_context_from_basename(basename: str, config: Dict[str, Any]) -> Optional[str]:
129
+ """Detect context by matching a sync basename against prompts_dir prefixes or paths patterns."""
130
+ if not basename:
131
+ return None
132
+
133
+ contexts = config.get('contexts', {})
134
+ matches = []
135
+
136
+ for context_name, context_config in contexts.items():
137
+ if context_name == 'default':
138
+ continue
139
+
140
+ defaults = context_config.get('defaults', {})
141
+ prompts_dir = defaults.get('prompts_dir', '')
142
+ if prompts_dir:
143
+ normalized = prompts_dir.rstrip('/')
144
+ prefix = normalized
145
+ if normalized == 'prompts':
146
+ prefix = ''
147
+ elif normalized.startswith('prompts/'):
148
+ prefix = normalized[len('prompts/'):]
149
+
150
+ if prefix and (basename == prefix or basename.startswith(prefix + '/')):
151
+ matches.append((context_name, len(prefix)))
152
+ continue
153
+
154
+ for path_pattern in context_config.get('paths', []):
155
+ pattern_base = path_pattern.rstrip('/**').rstrip('/*')
156
+ if fnmatch.fnmatch(basename, path_pattern) or \
157
+ basename.startswith(pattern_base + '/') or \
158
+ basename == pattern_base:
159
+ matches.append((context_name, len(pattern_base)))
160
+
161
+ if not matches:
162
+ return None
163
+
164
+ matches.sort(key=lambda item: item[1], reverse=True)
165
+ return matches[0][0]
166
+
167
+
168
+ def _get_relative_basename(input_path: str, pattern: str) -> str:
169
+ """
170
+ Compute basename relative to the matched pattern base.
171
+
172
+ This is critical for Issue #237: when a context pattern like
173
+ 'frontend/components/**' matches 'frontend/components/marketplace/AssetCard',
174
+ we need to return 'marketplace/AssetCard' (relative to pattern base),
175
+ not the full path which would cause double-pathing in output.
176
+
177
+ Args:
178
+ input_path: The full input path (e.g., 'frontend/components/marketplace/AssetCard')
179
+ pattern: The matching pattern (e.g., 'frontend/components/**')
180
+
181
+ Returns:
182
+ Path relative to the pattern base (e.g., 'marketplace/AssetCard')
183
+
184
+ Examples:
185
+ >>> _get_relative_basename('frontend/components/marketplace/AssetCard', 'frontend/components/**')
186
+ 'marketplace/AssetCard'
187
+ >>> _get_relative_basename('backend/utils/credit_helpers', 'backend/utils/**')
188
+ 'credit_helpers'
189
+ >>> _get_relative_basename('unknown/path', 'other/**')
190
+ 'unknown/path' # No match, return as-is
191
+ """
192
+ # Strip glob patterns to get the base directory
193
+ pattern_base = pattern.rstrip('/**').rstrip('/*').rstrip('*')
194
+
195
+ # Remove trailing slash from pattern base if present
196
+ pattern_base = pattern_base.rstrip('/')
197
+
198
+ # Check if input path starts with pattern base
199
+ if input_path.startswith(pattern_base + '/'):
200
+ # Return the portion after pattern_base/
201
+ return input_path[len(pattern_base) + 1:]
202
+ elif input_path.startswith(pattern_base) and len(input_path) > len(pattern_base):
203
+ # Handle case where pattern_base has no trailing content
204
+ remainder = input_path[len(pattern_base):]
205
+ if remainder.startswith('/'):
206
+ return remainder[1:]
207
+ return remainder
208
+ elif input_path == pattern_base:
209
+ # Exact match - return just the last component
210
+ return input_path.split('/')[-1] if '/' in input_path else input_path
211
+
212
+ # No match - return as-is (fallback for default context)
213
+ return input_path
214
+
215
+
76
216
  def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
77
217
  """Detect the appropriate context based on current directory path."""
78
218
  if context_override:
@@ -82,28 +222,82 @@ def _detect_context(current_dir: Path, config: Dict[str, Any], context_override:
82
222
  available = list(contexts.keys())
83
223
  raise ValueError(f"Unknown context '{context_override}'. Available contexts: {available}")
84
224
  return context_override
85
-
225
+
86
226
  contexts = config.get('contexts', {})
87
- current_path_str = str(current_dir)
88
-
89
- # Try to match against each context's paths
227
+ return _match_path_to_contexts(str(current_dir), contexts, use_specificity=False, is_absolute=True)
228
+
229
+
230
+ def detect_context_for_file(file_path: str, repo_root: Optional[str] = None) -> Tuple[Optional[str], Dict[str, Any]]:
231
+ """
232
+ Detect the appropriate context for a file path based on .pddrc configuration.
233
+
234
+ This function finds the most specific matching context by comparing pattern lengths.
235
+ For example, 'backend/functions/utils/**' is more specific than 'backend/**'.
236
+
237
+ Args:
238
+ file_path: Path to the file (can be absolute or relative)
239
+ repo_root: Optional repository root path. If not provided, will be detected.
240
+
241
+ Returns:
242
+ Tuple of (context_name, context_config_defaults) or (None, {}) if no match.
243
+ """
244
+ # Find repo root if not provided
245
+ if repo_root is None:
246
+ pddrc_path = _find_pddrc_file(Path(file_path).parent)
247
+ if pddrc_path:
248
+ repo_root = str(pddrc_path.parent)
249
+ else:
250
+ try:
251
+ import git
252
+ repo = git.Repo(file_path, search_parent_directories=True)
253
+ repo_root = repo.working_tree_dir
254
+ except:
255
+ repo_root = os.getcwd()
256
+
257
+ # Make file_path relative to repo_root for matching
258
+ file_path_abs = os.path.abspath(file_path)
259
+ repo_root_abs = os.path.abspath(repo_root)
260
+
261
+ if file_path_abs.startswith(repo_root_abs):
262
+ relative_path = os.path.relpath(file_path_abs, repo_root_abs)
263
+ else:
264
+ relative_path = file_path
265
+
266
+ # Find and load .pddrc
267
+ pddrc_path = _find_pddrc_file(Path(repo_root))
268
+ if not pddrc_path:
269
+ return None, {}
270
+
271
+ try:
272
+ config = _load_pddrc_config(pddrc_path)
273
+ except ValueError:
274
+ return None, {}
275
+
276
+ contexts = config.get('contexts', {})
277
+
278
+ # First, try to match against prompts_dir for each context
279
+ # This allows prompt files to be detected even when paths pattern only matches code files
280
+ prompts_dir_matches = []
90
281
  for context_name, context_config in contexts.items():
91
282
  if context_name == 'default':
92
- continue # Handle default as fallback
93
-
94
- paths = context_config.get('paths', [])
95
- for path_pattern in paths:
96
- # Convert glob pattern to match current directory
97
- if fnmatch.fnmatch(current_path_str, f"*/{path_pattern}") or \
98
- fnmatch.fnmatch(current_path_str, path_pattern) or \
99
- current_path_str.endswith(f"/{path_pattern.rstrip('/**')}"):
100
- return context_name
101
-
102
- # Return default context if available
103
- if 'default' in contexts:
104
- return 'default'
105
-
106
- return None
283
+ continue
284
+ prompts_dir = context_config.get('defaults', {}).get('prompts_dir', '')
285
+ if prompts_dir:
286
+ prompts_dir_normalized = prompts_dir.rstrip('/')
287
+ if relative_path.startswith(prompts_dir_normalized + '/') or relative_path == prompts_dir_normalized:
288
+ # Track match with specificity (length of prompts_dir)
289
+ prompts_dir_matches.append((context_name, len(prompts_dir_normalized)))
290
+
291
+ # Return most specific prompts_dir match if any
292
+ if prompts_dir_matches:
293
+ prompts_dir_matches.sort(key=lambda x: x[1], reverse=True)
294
+ matched_context = prompts_dir_matches[0][0]
295
+ return matched_context, _get_context_config(config, matched_context)
296
+
297
+ # Fall back to existing paths pattern matching
298
+ context_name = _match_path_to_contexts(relative_path, contexts, use_specificity=True, is_absolute=False)
299
+ return context_name, _get_context_config(config, context_name)
300
+
107
301
 
108
302
  def _get_context_config(config: Dict[str, Any], context_name: Optional[str]) -> Dict[str, Any]:
109
303
  """Get configuration settings for the specified context."""
@@ -121,7 +315,7 @@ def _resolve_config_hierarchy(
121
315
  ) -> Dict[str, Any]:
122
316
  """Apply configuration hierarchy: CLI > context > environment > defaults."""
123
317
  resolved = {}
124
-
318
+
125
319
  # Configuration keys to resolve
126
320
  config_keys = {
127
321
  'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
@@ -135,7 +329,7 @@ def _resolve_config_hierarchy(
135
329
  'budget': None,
136
330
  'max_attempts': None,
137
331
  }
138
-
332
+
139
333
  for config_key, env_var in config_keys.items():
140
334
  # 1. CLI options (highest priority)
141
335
  if config_key in cli_options and cli_options[config_key] is not None:
@@ -147,10 +341,56 @@ def _resolve_config_hierarchy(
147
341
  elif env_var and env_var in env_vars:
148
342
  resolved[config_key] = env_vars[env_var]
149
343
  # 4. Defaults are handled elsewhere
150
-
344
+
345
+ # Issue #237: Pass through 'outputs' config for template-based path generation
346
+ # This enables extensible project layouts (Next.js, Vue, Python, Go, etc.)
347
+ if 'outputs' in context_config:
348
+ resolved['outputs'] = context_config['outputs']
349
+
151
350
  return resolved
152
351
 
153
352
 
353
+ def get_tests_dir_from_config(start_path: Optional[Path] = None) -> Optional[Path]:
354
+ """
355
+ Get the tests directory from .pddrc configuration.
356
+
357
+ Searches for .pddrc, detects the appropriate context, and returns the
358
+ configured test_output_path as a Path object.
359
+
360
+ Args:
361
+ start_path: Starting directory for .pddrc search. Defaults to CWD.
362
+
363
+ Returns:
364
+ Path to tests directory if configured, None otherwise.
365
+ """
366
+ if start_path is None:
367
+ start_path = Path.cwd()
368
+
369
+ # Find and load .pddrc
370
+ pddrc_path = _find_pddrc_file(start_path)
371
+ if not pddrc_path:
372
+ return None
373
+
374
+ try:
375
+ config = _load_pddrc_config(pddrc_path)
376
+ except ValueError:
377
+ return None
378
+
379
+ # Detect context and get its config
380
+ context_name = _detect_context(start_path, config)
381
+ context_config = _get_context_config(config, context_name)
382
+
383
+ # Check context config first, then env var
384
+ test_output_path = context_config.get('test_output_path')
385
+ if not test_output_path:
386
+ test_output_path = os.environ.get('PDD_TEST_OUTPUT_PATH')
387
+
388
+ if test_output_path:
389
+ return Path(test_output_path)
390
+
391
+ return None
392
+
393
+
154
394
  def _read_file(path: Path) -> str:
155
395
  """Read a text file safely and return its contents."""
156
396
  try:
@@ -463,9 +703,30 @@ def construct_paths(
463
703
  pddrc_config = _load_pddrc_config(pddrc_path)
464
704
 
465
705
  # Detect appropriate context
466
- current_dir = Path.cwd()
467
- context = _detect_context(current_dir, pddrc_config, context_override)
468
-
706
+ # Priority: context_override > file-based detection > CWD-based detection
707
+ if context_override:
708
+ # Delegate validation to _detect_context to avoid duplicate validation logic
709
+ context = _detect_context(Path.cwd(), pddrc_config, context_override)
710
+ else:
711
+ # Try file-based detection when prompt file is provided
712
+ prompt_file_str = input_file_paths.get('prompt_file') if input_file_paths else None
713
+ if prompt_file_str and Path(prompt_file_str).exists():
714
+ detected_context, _ = detect_context_for_file(prompt_file_str)
715
+ if detected_context:
716
+ context = detected_context
717
+ else:
718
+ context = _detect_context(Path.cwd(), pddrc_config, None)
719
+ else:
720
+ basename_hint = command_options.get("basename")
721
+ if basename_hint:
722
+ detected_context = _detect_context_from_basename(basename_hint, pddrc_config)
723
+ if detected_context:
724
+ context = detected_context
725
+ else:
726
+ context = _detect_context(Path.cwd(), pddrc_config, None)
727
+ else:
728
+ context = _detect_context(Path.cwd(), pddrc_config, None)
729
+
469
730
  # Get context-specific configuration
470
731
  context_config = _get_context_config(pddrc_config, context)
471
732
  original_context_config = context_config.copy() # Store original before modifications
@@ -476,9 +737,15 @@ def construct_paths(
476
737
  # Apply configuration hierarchy
477
738
  env_vars = dict(os.environ)
478
739
  resolved_config = _resolve_config_hierarchy(command_options, context_config, env_vars)
479
-
740
+
741
+ # Issue #237: Track matched context for debugging
742
+ resolved_config['_matched_context'] = context or 'default'
743
+
480
744
  # Update command_options with resolved configuration for internal use
745
+ # Exclude internal metadata keys (prefixed with _) from command_options
481
746
  for key, value in resolved_config.items():
747
+ if key.startswith('_'):
748
+ continue # Skip internal metadata like _matched_context
482
749
  if key not in command_options or command_options[key] is None:
483
750
  command_options[key] = value
484
751
 
@@ -531,7 +798,10 @@ def construct_paths(
531
798
  else:
532
799
  # Fall back to context-aware logic
533
800
  # Use original_context_config to avoid checking augmented config with env vars
534
- if original_context_config and any(key.endswith('_output_path') for key in original_context_config):
801
+ if original_context_config and (
802
+ 'prompts_dir' in original_context_config or
803
+ any(key.endswith('_output_path') for key in original_context_config)
804
+ ):
535
805
  # For configured contexts, use prompts_dir from config if provided,
536
806
  # otherwise default to "prompts" at the same level as output dirs
537
807
  resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
@@ -543,9 +813,22 @@ def construct_paths(
543
813
  resolved_config["code_dir"] = str(gen_path.parent)
544
814
 
545
815
  resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
546
- # example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
816
+
817
+ # Determine examples_dir for auto-deps scanning
818
+ # NOTE: outputs.example.path is for OUTPUT only (where to write examples),
819
+ # NOT for determining scan scope. Using it caused CSV row deletion issues.
820
+ # Check RAW context config for example_output_path, or default to "context".
821
+ # Do NOT use output_paths_str since generate_output_paths always returns absolute paths.
822
+ example_path_str = None
823
+ if original_context_config:
824
+ example_path_str = original_context_config.get("example_output_path")
825
+
826
+ # Final fallback to "context" (sensible default for this project)
827
+ if not example_path_str:
828
+ example_path_str = "context"
829
+
830
+ # example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
547
831
  # If it ends with / or has no file extension, treat as directory; otherwise use parent
548
- example_path_str = output_paths_str.get("example_output_path", "examples")
549
832
  example_path = Path(example_path_str)
550
833
  if example_path_str.endswith('/') or '.' not in example_path.name:
551
834
  resolved_config["examples_dir"] = example_path_str.rstrip('/')
@@ -633,7 +916,13 @@ def construct_paths(
633
916
 
634
917
  # ------------- Step 2: basename --------------------------
635
918
  try:
636
- basename = _extract_basename(command, input_paths)
919
+ # For sync, example, and test commands, prefer the basename from command_options if provided.
920
+ # This preserves subdirectory paths like 'core/cloud' which would otherwise
921
+ # be lost when extracting from the prompt file path.
922
+ if command in ("sync", "example", "test") and command_options.get("basename"):
923
+ basename = command_options["basename"]
924
+ else:
925
+ basename = _extract_basename(command, input_paths)
637
926
  except ValueError as exc:
638
927
  # Check if it's the specific error from the initial check (now done at start)
639
928
  # This try/except might not be needed if initial check is robust
@@ -854,9 +1143,22 @@ def construct_paths(
854
1143
  resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
855
1144
  resolved_config["code_dir"] = str(gen_path.parent)
856
1145
  resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
857
- # example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
1146
+
1147
+ # Determine examples_dir for auto-deps scanning
1148
+ # NOTE: outputs.example.path is for OUTPUT only (where to write examples),
1149
+ # NOT for determining scan scope. Using it caused CSV row deletion issues.
1150
+ # Check RAW context config for example_output_path, or default to "context".
1151
+ # Do NOT use resolved_config since generate_output_paths sets it to absolute paths.
1152
+ example_path_str = None
1153
+ if original_context_config:
1154
+ example_path_str = original_context_config.get("example_output_path")
1155
+
1156
+ # Final fallback to "context" (sensible default for this project)
1157
+ if not example_path_str:
1158
+ example_path_str = "context"
1159
+
1160
+ # example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
858
1161
  # If it ends with / or has no file extension, treat as directory; otherwise use parent
859
- example_path_str = resolved_config.get("example_output_path", "examples")
860
1162
  example_path = Path(example_path_str)
861
1163
  if example_path_str.endswith('/') or '.' not in example_path.name:
862
1164
  resolved_config["examples_dir"] = example_path_str.rstrip('/')