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
@@ -0,0 +1,537 @@
1
+ """
2
+ Orchestrator for the 12-step agentic change workflow.
3
+ Runs each step as a separate agentic task, accumulates context, tracks progress/cost,
4
+ and supports resuming from saved state. Includes a review loop (steps 10-11).
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Tuple, Any
14
+
15
+ from rich.console import Console
16
+ from rich.markup import escape
17
+
18
+ from pdd.agentic_common import (
19
+ run_agentic_task,
20
+ load_workflow_state,
21
+ save_workflow_state,
22
+ clear_workflow_state,
23
+ DEFAULT_MAX_RETRIES,
24
+ )
25
+ from pdd.load_prompt_template import load_prompt_template
26
+
27
+ # Initialize console for rich output
28
+ console = Console()
29
+
30
+ # Per-Step Timeouts (Workflow specific)
31
+ CHANGE_STEP_TIMEOUTS: Dict[int, float] = {
32
+ 1: 240.0, # Duplicate Check
33
+ 2: 240.0, # Docs Comparison
34
+ 3: 340.0, # Research
35
+ 4: 340.0, # Clarify
36
+ 5: 340.0, # Docs Changes
37
+ 6: 340.0, # Identify Dev Units
38
+ 7: 340.0, # Architecture Review
39
+ 8: 600.0, # Analyze Prompt Changes (Complex)
40
+ 9: 1000.0, # Implement Changes (Most Complex)
41
+ 10: 340.0, # Identify Issues
42
+ 11: 600.0, # Fix Issues (Complex)
43
+ 12: 340.0, # Create PR
44
+ }
45
+
46
+ MAX_REVIEW_ITERATIONS = 5
47
+
48
+ def _get_git_root(cwd: Path) -> Optional[Path]:
49
+ """Get repo root via git rev-parse."""
50
+ try:
51
+ result = subprocess.run(
52
+ ["git", "rev-parse", "--show-toplevel"],
53
+ cwd=cwd,
54
+ capture_output=True,
55
+ text=True,
56
+ check=True
57
+ )
58
+ return Path(result.stdout.strip())
59
+ except subprocess.CalledProcessError:
60
+ return None
61
+
62
+ def _setup_worktree(cwd: Path, issue_number: int, quiet: bool) -> Tuple[Optional[Path], Optional[str]]:
63
+ """
64
+ Create an isolated git worktree for the issue.
65
+ Returns (worktree_path, error_message).
66
+ """
67
+ git_root = _get_git_root(cwd)
68
+ if not git_root:
69
+ return None, "Not a git repository"
70
+
71
+ branch_name = f"change/issue-{issue_number}"
72
+ worktree_rel_path = Path(".pdd") / "worktrees" / f"change-issue-{issue_number}"
73
+ worktree_path = git_root / worktree_rel_path
74
+
75
+ # Clean up existing directory if it exists but isn't a valid worktree
76
+ if worktree_path.exists():
77
+ # Check if it's a valid worktree
78
+ is_worktree = False
79
+ try:
80
+ wt_list = subprocess.run(
81
+ ["git", "worktree", "list", "--porcelain"],
82
+ cwd=git_root,
83
+ capture_output=True,
84
+ text=True
85
+ ).stdout
86
+ if str(worktree_path) in wt_list:
87
+ is_worktree = True
88
+ except Exception:
89
+ pass
90
+
91
+ if is_worktree:
92
+ # Remove existing worktree to start fresh or ensure clean state
93
+ subprocess.run(
94
+ ["git", "worktree", "remove", "--force", str(worktree_path)],
95
+ cwd=git_root,
96
+ capture_output=True
97
+ )
98
+ else:
99
+ # Just a directory
100
+ shutil.rmtree(worktree_path)
101
+
102
+ # Clean up branch if it exists
103
+ try:
104
+ subprocess.run(
105
+ ["git", "branch", "-D", branch_name],
106
+ cwd=git_root,
107
+ capture_output=True
108
+ )
109
+ except Exception:
110
+ pass
111
+
112
+ # Create worktree
113
+ try:
114
+ worktree_path.parent.mkdir(parents=True, exist_ok=True)
115
+ subprocess.run(
116
+ ["git", "worktree", "add", "-b", branch_name, str(worktree_path), "HEAD"],
117
+ cwd=git_root,
118
+ capture_output=True,
119
+ check=True
120
+ )
121
+ if not quiet:
122
+ console.print(f"[blue]Working in worktree: {worktree_path}[/blue]")
123
+ return worktree_path, None
124
+ except subprocess.CalledProcessError as e:
125
+ return None, f"Git worktree creation failed: {e}"
126
+
127
+ def _parse_changed_files(output: str) -> List[str]:
128
+ """Extract file paths from FILES_CREATED or FILES_MODIFIED lines."""
129
+ files = []
130
+ # Look for FILES_CREATED: path, path
131
+ created_match = re.search(r"FILES_CREATED:\s*(.*)", output)
132
+ if created_match:
133
+ files.extend([f.strip() for f in created_match.group(1).split(",") if f.strip()])
134
+
135
+ # Look for FILES_MODIFIED: path, path
136
+ modified_match = re.search(r"FILES_MODIFIED:\s*(.*)", output)
137
+ if modified_match:
138
+ files.extend([f.strip() for f in modified_match.group(1).split(",") if f.strip()])
139
+
140
+ return list(set(files)) # Deduplicate
141
+
142
+ def _check_hard_stop(step_num: int, output: str) -> Optional[str]:
143
+ """Check output for hard stop conditions."""
144
+ if step_num == 1 and "Duplicate of #" in output:
145
+ return "Issue is a duplicate"
146
+ if step_num == 2 and "Already Implemented" in output:
147
+ return "Already implemented"
148
+ if step_num == 4 and "Clarification Needed" in output:
149
+ return "Clarification needed"
150
+ if step_num == 6 and "No Dev Units Found" in output:
151
+ return "No dev units found"
152
+ if step_num == 7 and "Architectural Decision Needed" in output:
153
+ return "Architectural decision needed"
154
+ if step_num == 8 and "No Changes Required" in output:
155
+ return "No changes needed"
156
+ if step_num == 9:
157
+ if "FAIL:" in output:
158
+ return "Implementation failed"
159
+ # Note: Missing FILES_... check is handled in logic, not just string match
160
+ return None
161
+
162
+ def _get_state_dir(cwd: Path) -> Path:
163
+ """Get the state directory relative to git root."""
164
+ root = _get_git_root(cwd) or cwd
165
+ return root / ".pdd" / "change-state"
166
+
167
+ def run_agentic_change_orchestrator(
168
+ issue_url: str,
169
+ issue_content: str,
170
+ repo_owner: str,
171
+ repo_name: str,
172
+ issue_number: int,
173
+ issue_author: str,
174
+ issue_title: str,
175
+ *,
176
+ cwd: Path,
177
+ verbose: bool = False,
178
+ quiet: bool = False,
179
+ timeout_adder: float = 0.0,
180
+ use_github_state: bool = True
181
+ ) -> Tuple[bool, str, float, str, List[str]]:
182
+ """
183
+ Orchestrates the 12-step agentic change workflow.
184
+
185
+ Returns:
186
+ (success, final_message, total_cost, model_used, changed_files)
187
+ """
188
+
189
+ if not quiet:
190
+ console.print(f"Implementing change for issue #{issue_number}: \"{issue_title}\"")
191
+
192
+ state_dir = _get_state_dir(cwd)
193
+
194
+ # Load state
195
+ state, loaded_gh_id = load_workflow_state(
196
+ cwd, issue_number, "change", state_dir, repo_owner, repo_name, use_github_state
197
+ )
198
+
199
+ # Initialize variables from state or defaults
200
+ if state is not None:
201
+ last_completed_step = state.get("last_completed_step", 0)
202
+ step_outputs = state.get("step_outputs", {})
203
+ total_cost = state.get("total_cost", 0.0)
204
+ model_used = state.get("model_used", "unknown")
205
+ github_comment_id = loaded_gh_id # Use the ID returned by load_workflow_state
206
+ worktree_path_str = state.get("worktree_path")
207
+ worktree_path = Path(worktree_path_str) if worktree_path_str else None
208
+ else:
209
+ # Initialize fresh state dict for new workflow
210
+ state = {"step_outputs": {}}
211
+ last_completed_step = 0
212
+ step_outputs = state["step_outputs"]
213
+ total_cost = 0.0
214
+ model_used = "unknown"
215
+ github_comment_id = None
216
+ worktree_path = None
217
+
218
+ # Context accumulation dictionary
219
+ context = {
220
+ "issue_url": issue_url,
221
+ "issue_content": issue_content,
222
+ "repo_owner": repo_owner,
223
+ "repo_name": repo_name,
224
+ "issue_number": issue_number,
225
+ "issue_author": issue_author,
226
+ "issue_title": issue_title,
227
+ }
228
+
229
+ # Populate context with cached outputs
230
+ for s_num, s_out in step_outputs.items():
231
+ context[f"step{s_num}_output"] = s_out
232
+
233
+ # Determine start step
234
+ start_step = last_completed_step + 1
235
+
236
+ if last_completed_step > 0 and not quiet:
237
+ console.print(f"Resuming change workflow for issue #{issue_number}")
238
+ console.print(f" Steps 1-{last_completed_step} already complete (cached)")
239
+ console.print(f" Starting from Step {start_step}")
240
+
241
+ # --- Steps 1 through 9 ---
242
+
243
+ # Step definitions for 1-9
244
+ steps_config = [
245
+ (1, "duplicate", "Search for duplicate issues"),
246
+ (2, "docs", "Check if already implemented"),
247
+ (3, "research", "Research to clarify specifications"),
248
+ (4, "clarify", "Verify requirements are clear"),
249
+ (5, "docs_change", "Analyze documentation changes needed"),
250
+ (6, "devunits", "Identify dev units involved"),
251
+ (7, "architecture", "Review architecture"),
252
+ (8, "analyze", "Analyze prompt changes"),
253
+ (9, "implement", "Implement the prompt changes"),
254
+ ]
255
+
256
+ current_work_dir = cwd
257
+ changed_files = []
258
+
259
+ # If we are resuming at step 9 or later, we need to ensure the worktree exists/is active
260
+ if start_step >= 9:
261
+ # If we have a path in state, verify it exists, otherwise recreate
262
+ if worktree_path and worktree_path.exists():
263
+ if not quiet:
264
+ console.print(f"[blue]Reusing existing worktree: {worktree_path}[/blue]")
265
+ current_work_dir = worktree_path
266
+ else:
267
+ # Re-create worktree if missing
268
+ wt_path, err = _setup_worktree(cwd, issue_number, quiet)
269
+ if not wt_path:
270
+ return False, f"Failed to restore worktree: {err}", total_cost, model_used, []
271
+ worktree_path = wt_path
272
+ current_work_dir = worktree_path
273
+ # Update state with new path
274
+ state["worktree_path"] = str(worktree_path)
275
+
276
+ for step_num, name, description in steps_config:
277
+ # Skip if already done
278
+ if step_num < start_step:
279
+ continue
280
+
281
+ # Special handling before Step 9: Create Worktree
282
+ if step_num == 9:
283
+ # Check current branch before creating worktree
284
+ try:
285
+ current_branch = subprocess.run(
286
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
287
+ cwd=cwd,
288
+ capture_output=True,
289
+ text=True,
290
+ check=True
291
+ ).stdout.strip()
292
+
293
+ if current_branch not in ["main", "master"] and not quiet:
294
+ console.print(f"[yellow]Note: Creating branch from HEAD ({current_branch}), not origin/main. PR will include commits from this branch. Run from main for independent changes.[/yellow]")
295
+ except subprocess.CalledProcessError:
296
+ pass # Ignore if git command fails, worktree setup will likely catch issues
297
+
298
+ wt_path, err = _setup_worktree(cwd, issue_number, quiet)
299
+ if not wt_path:
300
+ return False, f"Failed to create worktree: {err}", total_cost, model_used, []
301
+ worktree_path = wt_path
302
+ current_work_dir = worktree_path
303
+ state["worktree_path"] = str(worktree_path)
304
+ context["worktree_path"] = str(worktree_path)
305
+
306
+ if not quiet:
307
+ console.print(f"[bold][Step {step_num}/12][/bold] {description}...")
308
+
309
+ # Load Prompt
310
+ template_name = f"agentic_change_step{step_num}_{name}_LLM"
311
+ prompt_template = load_prompt_template(template_name)
312
+ if not prompt_template:
313
+ return False, f"Missing prompt template: {template_name}", total_cost, model_used, []
314
+
315
+ # Format Prompt
316
+ try:
317
+ formatted_prompt = prompt_template.format(**context)
318
+ except KeyError as e:
319
+ return False, f"Context missing key for step {step_num}: {e}", total_cost, model_used, []
320
+
321
+ # Run Task
322
+ timeout = CHANGE_STEP_TIMEOUTS.get(step_num, 340.0) + timeout_adder
323
+ step_success, step_output, step_cost, step_model = run_agentic_task(
324
+ instruction=formatted_prompt,
325
+ cwd=current_work_dir,
326
+ verbose=verbose,
327
+ quiet=quiet,
328
+ timeout=timeout,
329
+ label=f"step{step_num}",
330
+ max_retries=DEFAULT_MAX_RETRIES,
331
+ )
332
+
333
+ # Update tracking
334
+ total_cost += step_cost
335
+ model_used = step_model
336
+ state["total_cost"] = total_cost
337
+ state["model_used"] = model_used
338
+
339
+ if not step_success:
340
+ # Check if it's a hard stop condition that caused \"failure\" or just agent error
341
+ stop_reason = _check_hard_stop(step_num, step_output)
342
+ if stop_reason:
343
+ if not quiet:
344
+ console.print(f"[yellow]Investigation stopped at Step {step_num}: {stop_reason}[/yellow]")
345
+ # Save state so we don't re-run previous steps
346
+ state["last_completed_step"] = step_num
347
+ state["step_outputs"][str(step_num)] = step_output
348
+ save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
349
+ return False, f"Stopped at step {step_num}: {stop_reason}", total_cost, model_used, []
350
+
351
+ # Soft failure
352
+ console.print(f"[yellow]Warning: Step {step_num} reported failure but continuing...[/yellow]")
353
+
354
+ # Check hard stops on success too
355
+ stop_reason = _check_hard_stop(step_num, step_output)
356
+ if stop_reason:
357
+ if not quiet:
358
+ console.print(f"[yellow]Investigation stopped at Step {step_num}: {stop_reason}[/yellow]")
359
+ state["last_completed_step"] = step_num
360
+ state["step_outputs"][str(step_num)] = step_output
361
+ save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
362
+ return False, f"Stopped at step {step_num}: {stop_reason}", total_cost, model_used, []
363
+
364
+ # Step 9 specific: Parse files
365
+ if step_num == 9:
366
+ extracted_files = _parse_changed_files(step_output)
367
+ changed_files = extracted_files
368
+ context["files_to_stage"] = ", ".join(changed_files)
369
+
370
+ if not changed_files:
371
+ # Hard stop if implementation produced no file changes
372
+ return False, "Stopped at step 9: Implementation produced no file changes", total_cost, model_used, []
373
+
374
+ # Update Context & State
375
+ # Only mark step completed if it succeeded; failed steps get "FAILED:" prefix
376
+ # and last_completed_step stays at previous step (ensures resume re-runs failed step)
377
+ context[f"step{step_num}_output"] = step_output
378
+ if step_success:
379
+ state["step_outputs"][str(step_num)] = step_output
380
+ state["last_completed_step"] = step_num
381
+ else:
382
+ state["step_outputs"][str(step_num)] = f"FAILED: {step_output}"
383
+ # Don't update last_completed_step - keep it at previous value
384
+
385
+ # Save State
386
+ save_result = save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
387
+ if save_result:
388
+ github_comment_id = save_result
389
+ state["github_comment_id"] = github_comment_id
390
+
391
+ if not quiet:
392
+ # Brief result summary
393
+ lines = step_output.strip().split('\n')
394
+ brief = lines[-1] if lines else "Done"
395
+ if len(brief) > 80: brief = brief[:77] + "..."
396
+ console.print(f" -> {escape(brief)}")
397
+
398
+ # --- Review Loop (Steps 10-11) ---
399
+
400
+ # Ensure we have files_to_stage if we resumed after step 9
401
+ if "files_to_stage" not in context:
402
+ # Try to recover from step 9 output
403
+ s9_out = context.get("step9_output", "")
404
+ c_files = _parse_changed_files(s9_out)
405
+ changed_files = c_files
406
+ context["files_to_stage"] = ", ".join(c_files)
407
+
408
+ review_iteration = state.get("review_iteration", 0)
409
+ previous_fixes = state.get("previous_fixes", "")
410
+
411
+ # If we haven't finished the review loop (i.e., we haven't reached step 12 yet)
412
+ if last_completed_step < 12:
413
+ while review_iteration < MAX_REVIEW_ITERATIONS:
414
+ review_iteration += 1
415
+ state["review_iteration"] = review_iteration
416
+
417
+ # --- Step 10: Identify Issues ---
418
+ if not quiet:
419
+ console.print(f"[bold][Step 10/12][/bold] Identifying issues (iteration {review_iteration}/{MAX_REVIEW_ITERATIONS})...")
420
+
421
+ s10_template = load_prompt_template("agentic_change_step10_identify_issues_LLM")
422
+ context["review_iteration"] = review_iteration
423
+ context["previous_fixes"] = previous_fixes
424
+
425
+ s10_prompt = s10_template.format(**context)
426
+
427
+ timeout10 = CHANGE_STEP_TIMEOUTS.get(10, 340.0) + timeout_adder
428
+ s10_success, s10_output, s10_cost, s10_model = run_agentic_task(
429
+ instruction=s10_prompt,
430
+ cwd=current_work_dir,
431
+ verbose=verbose,
432
+ quiet=quiet,
433
+ timeout=timeout10,
434
+ label=f"step10_iter{review_iteration}",
435
+ max_retries=DEFAULT_MAX_RETRIES,
436
+ )
437
+
438
+ total_cost += s10_cost
439
+ model_used = s10_model
440
+ state["total_cost"] = total_cost
441
+
442
+ if "No Issues Found" in s10_output:
443
+ if not quiet:
444
+ console.print(" -> No issues found. Proceeding to PR.")
445
+ break
446
+
447
+ if not quiet:
448
+ console.print(" -> Issues found. Proceeding to fix.")
449
+
450
+ # --- Step 11: Fix Issues ---
451
+ if not quiet:
452
+ console.print(f"[bold][Step 11/12][/bold] Fixing issues (iteration {review_iteration}/{MAX_REVIEW_ITERATIONS})...")
453
+
454
+ s11_template = load_prompt_template("agentic_change_step11_fix_issues_LLM")
455
+ context["step10_output"] = s10_output
456
+
457
+ s11_prompt = s11_template.format(**context)
458
+
459
+ timeout11 = CHANGE_STEP_TIMEOUTS.get(11, 600.0) + timeout_adder
460
+ s11_success, s11_output, s11_cost, s11_model = run_agentic_task(
461
+ instruction=s11_prompt,
462
+ cwd=current_work_dir,
463
+ verbose=verbose,
464
+ quiet=quiet,
465
+ timeout=timeout11,
466
+ label=f"step11_iter{review_iteration}",
467
+ max_retries=DEFAULT_MAX_RETRIES,
468
+ )
469
+
470
+ total_cost += s11_cost
471
+ model_used = s11_model
472
+ state["total_cost"] = total_cost
473
+
474
+ previous_fixes += f"\n\nIteration {review_iteration}:\n{s11_output}"
475
+ state["previous_fixes"] = previous_fixes
476
+
477
+ # Save state inside loop
478
+ save_result = save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
479
+ if save_result:
480
+ github_comment_id = save_result
481
+ state["github_comment_id"] = github_comment_id
482
+
483
+ if review_iteration >= MAX_REVIEW_ITERATIONS:
484
+ console.print("[yellow]Warning: Maximum review iterations reached. Proceeding to PR creation.[/yellow]")
485
+
486
+ # --- Step 12: Create PR ---
487
+ if last_completed_step < 12:
488
+ if not quiet:
489
+ console.print("[bold][Step 12/12][/bold] Create PR and link to issue...")
490
+
491
+ s12_template = load_prompt_template("agentic_change_step12_create_pr_LLM")
492
+ s12_prompt = s12_template.format(**context)
493
+
494
+ timeout12 = CHANGE_STEP_TIMEOUTS.get(12, 340.0) + timeout_adder
495
+ s12_success, s12_output, s12_cost, s12_model = run_agentic_task(
496
+ instruction=s12_prompt,
497
+ cwd=current_work_dir,
498
+ verbose=verbose,
499
+ quiet=quiet,
500
+ timeout=timeout12,
501
+ label="step12",
502
+ max_retries=DEFAULT_MAX_RETRIES,
503
+ )
504
+
505
+ total_cost += s12_cost
506
+ model_used = s12_model
507
+ state["total_cost"] = total_cost
508
+
509
+ if not s12_success:
510
+ console.print("[red]Step 12 (PR Creation) failed.[/red]")
511
+ # Save state to allow retry
512
+ save_workflow_state(cwd, issue_number, "change", state, state_dir, repo_owner, repo_name, use_github_state, github_comment_id)
513
+ return False, "PR Creation failed", total_cost, model_used, changed_files
514
+
515
+ # Extract PR URL if possible (simple heuristic)
516
+ pr_url = "Unknown"
517
+ url_match = re.search(r"https://github.com/\S+/pull/\d+", s12_output)
518
+ if url_match:
519
+ pr_url = url_match.group(0)
520
+
521
+ # Final Success
522
+ if not quiet:
523
+ console.print("\n[green]Change workflow complete[/green]")
524
+ console.print(f" Total cost: ${total_cost:.4f}")
525
+ console.print(f" Files changed: {', '.join(changed_files)}")
526
+ console.print(f" PR: {pr_url}")
527
+ console.print(f" Review iterations: {review_iteration}")
528
+ console.print("\nNext steps:")
529
+ console.print(" 1. Review and merge the PR")
530
+ console.print(" 2. Run `pdd sync <module>` after merge")
531
+
532
+ # Clear state on success
533
+ clear_workflow_state(cwd, issue_number, "change", state_dir, repo_owner, repo_name, use_github_state)
534
+
535
+ return True, f"PR Created: {pr_url}", total_cost, model_used, changed_files
536
+
537
+ return True, "Workflow already completed", total_cost, model_used, changed_files