pdd-cli 0.0.45__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 (195) hide show
  1. pdd/__init__.py +40 -8
  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 +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,426 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import time
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import List, Tuple, Dict, Any, Optional, Set
10
+
11
+ from rich.console import Console
12
+
13
+ from .agentic_common import (
14
+ run_agentic_task,
15
+ load_workflow_state,
16
+ save_workflow_state,
17
+ clear_workflow_state,
18
+ )
19
+ from .load_prompt_template import load_prompt_template
20
+
21
+ # Constants
22
+ STEP_NAMES = {
23
+ 1: "unit_tests",
24
+ 2: "e2e_tests",
25
+ 3: "root_cause",
26
+ 4: "fix_e2e_tests",
27
+ 5: "identify_devunits",
28
+ 6: "create_unit_tests",
29
+ 7: "verify_tests",
30
+ 8: "run_pdd_fix",
31
+ 9: "verify_all",
32
+ }
33
+
34
+ STEP_DESCRIPTIONS = {
35
+ 1: "Running unit tests from issue",
36
+ 2: "Running e2e tests",
37
+ 3: "Analyzing root cause",
38
+ 4: "Fixing e2e tests",
39
+ 5: "Identifying dev units",
40
+ 6: "Creating unit tests",
41
+ 7: "Verifying tests detect bugs",
42
+ 8: "Running pdd fix",
43
+ 9: "Final verification",
44
+ }
45
+
46
+ # Per-step timeouts for the 9-step agentic e2e fix workflow
47
+ E2E_FIX_STEP_TIMEOUTS: Dict[int, float] = {
48
+ 1: 340.0, # Run unit tests from issue, pdd fix failures
49
+ 2: 240.0, # Run e2e tests, check completion (early exit)
50
+ 3: 340.0, # Root cause analysis (code vs test vs both)
51
+ 4: 340.0, # Fix e2e tests if needed
52
+ 5: 340.0, # Identify dev units involved in failures
53
+ 6: 600.0, # Create/append unit tests for dev units (Complex)
54
+ 7: 600.0, # Verify unit tests detect bugs (Complex)
55
+ 8: 1000.0, # Run pdd fix on failing dev units (Most Complex - multiple LLM calls)
56
+ 9: 240.0, # Final verification, loop control
57
+ }
58
+
59
+ console = Console()
60
+
61
+ def _get_state_dir(cwd: Path) -> Path:
62
+ """Returns the state directory .pdd/e2e-fix-state/ relative to git root."""
63
+ # Simple heuristic: look for .git, otherwise use cwd
64
+ d = cwd.resolve()
65
+ root = d
66
+ while d != d.parent:
67
+ if (d / ".git").exists():
68
+ root = d
69
+ break
70
+ d = d.parent
71
+
72
+ state_dir = root / ".pdd" / "e2e-fix-state"
73
+ state_dir.mkdir(parents=True, exist_ok=True)
74
+ return state_dir
75
+
76
+ def _parse_changed_files(output: str) -> List[str]:
77
+ """Parses FILES_CREATED and FILES_MODIFIED from agent output."""
78
+ files = []
79
+ for line in output.splitlines():
80
+ if line.startswith("FILES_CREATED:") or line.startswith("FILES_MODIFIED:"):
81
+ # Extract content after colon
82
+ content = line.split(":", 1)[1].strip()
83
+ if content:
84
+ # Split by comma and strip
85
+ paths = [p.strip() for p in content.split(",") if p.strip()]
86
+ files.extend(paths)
87
+ return files
88
+
89
+ def _parse_dev_units(output: str) -> str:
90
+ """Parses DEV_UNITS_IDENTIFIED from output."""
91
+ for line in output.splitlines():
92
+ if line.startswith("DEV_UNITS_IDENTIFIED:"):
93
+ return line.split(":", 1)[1].strip()
94
+ return ""
95
+
96
+ def _update_dev_unit_states(output: str, current_states: Dict[str, Any], identified_units_str: str) -> Dict[str, Any]:
97
+ """Updates dev unit states based on Step 8 output."""
98
+ identified_units = [u.strip() for u in identified_units_str.split(",") if u.strip()]
99
+
100
+ # Initialize if not present
101
+ for unit in identified_units:
102
+ if unit not in current_states:
103
+ current_states[unit] = {"fixed": False, "fix_attempts": 0}
104
+ current_states[unit]["fix_attempts"] += 1
105
+
106
+ # Parse results from output
107
+ # Heuristic: look for "unit_name: FIXED" or "unit_name: Failed"
108
+ # This depends on the LLM following instructions in Step 8 prompt.
109
+ for line in output.splitlines():
110
+ for unit in identified_units:
111
+ if unit in line:
112
+ if "FIXED" in line:
113
+ current_states[unit]["fixed"] = True
114
+ elif "Failed" in line or "FAIL" in line:
115
+ current_states[unit]["fixed"] = False
116
+
117
+ return current_states
118
+
119
+ def _check_staleness(state: Dict[str, Any], cwd: Path) -> None:
120
+ """Checks if files have changed since state was saved."""
121
+ last_saved_str = state.get("last_saved_at")
122
+ if not last_saved_str:
123
+ return
124
+
125
+ try:
126
+ last_saved = datetime.fromisoformat(last_saved_str)
127
+ except ValueError:
128
+ return
129
+
130
+ changed_files = state.get("changed_files", [])
131
+ stale = False
132
+
133
+ for file_path in changed_files:
134
+ p = cwd / file_path
135
+ if not p.exists():
136
+ console.print(f"[yellow]Warning: File '{file_path}' from previous state is missing.[/yellow]")
137
+ continue
138
+
139
+ # Check mtime
140
+ mtime = datetime.fromtimestamp(p.stat().st_mtime)
141
+ if mtime > last_saved:
142
+ stale = True
143
+ break
144
+
145
+ if stale:
146
+ console.print("[yellow]Warning: Codebase may have changed since last run. Consider --no-resume for fresh start.[/yellow]")
147
+
148
+ def run_agentic_e2e_fix_orchestrator(
149
+ issue_url: str,
150
+ issue_content: str,
151
+ repo_owner: str,
152
+ repo_name: str,
153
+ issue_number: int,
154
+ issue_author: str,
155
+ issue_title: str,
156
+ *,
157
+ cwd: Path,
158
+ timeout_adder: float = 0.0,
159
+ max_cycles: int = 5,
160
+ resume: bool = True,
161
+ verbose: bool = False,
162
+ quiet: bool = False,
163
+ use_github_state: bool = True
164
+ ) -> Tuple[bool, str, float, str, List[str]]:
165
+ """
166
+ Orchestrator for the 9-step agentic e2e fix workflow.
167
+
168
+ Returns:
169
+ Tuple[bool, str, float, str, List[str]]:
170
+ (success, final_message, total_cost, model_used, changed_files)
171
+ """
172
+ state_dir = _get_state_dir(cwd)
173
+ workflow_name = "e2e_fix"
174
+
175
+ # Initialize state variables
176
+ current_cycle = 0
177
+ last_completed_step = 0
178
+ step_outputs: Dict[str, str] = {}
179
+ total_cost = 0.0
180
+ model_used = "unknown"
181
+ changed_files: List[str] = []
182
+ dev_unit_states: Dict[str, Any] = {}
183
+ github_comment_id: Optional[int] = None
184
+
185
+ # Resume Logic
186
+ if resume:
187
+ loaded_state, gh_id = load_workflow_state(
188
+ cwd, issue_number, workflow_name, state_dir, repo_owner, repo_name, use_github_state
189
+ )
190
+ if loaded_state:
191
+ console.print(f"[blue]Resuming from cycle {loaded_state.get('current_cycle', 1)} step {loaded_state.get('last_completed_step', 0)}...[/blue]")
192
+ current_cycle = loaded_state.get("current_cycle", 0)
193
+ last_completed_step = loaded_state.get("last_completed_step", 0)
194
+ step_outputs = loaded_state.get("step_outputs", {})
195
+ total_cost = loaded_state.get("total_cost", 0.0)
196
+ model_used = loaded_state.get("model_used", "unknown")
197
+ changed_files = loaded_state.get("changed_files", [])
198
+ dev_unit_states = loaded_state.get("dev_unit_states", {})
199
+ github_comment_id = gh_id
200
+
201
+ _check_staleness(loaded_state, cwd)
202
+
203
+ # If we finished a cycle but didn't exit, prepare for next cycle
204
+ if last_completed_step >= 9:
205
+ current_cycle += 1
206
+ last_completed_step = 0
207
+ step_outputs = {} # Clear outputs for new cycle
208
+ else:
209
+ # No state found, start fresh
210
+ clear_workflow_state(cwd, issue_number, workflow_name, state_dir, repo_owner, repo_name, use_github_state)
211
+ else:
212
+ clear_workflow_state(cwd, issue_number, workflow_name, state_dir, repo_owner, repo_name, use_github_state)
213
+
214
+ console.print(f"Fixing e2e tests for issue #{issue_number}: \"{issue_title}\"")
215
+
216
+ success = False
217
+ final_message = ""
218
+
219
+ try:
220
+ # Outer Loop
221
+ if current_cycle == 0:
222
+ current_cycle = 1
223
+
224
+ while current_cycle <= max_cycles:
225
+ console.print(f"\n[bold cyan][Cycle {current_cycle}/{max_cycles}] Starting fix cycle...[/bold cyan]")
226
+
227
+ # Inner Loop (Steps 1-9)
228
+ for step_num in range(1, 10):
229
+ if step_num <= last_completed_step:
230
+ continue # Skip already completed steps in this cycle
231
+
232
+ step_name = STEP_NAMES[step_num]
233
+ description = STEP_DESCRIPTIONS[step_num]
234
+
235
+ console.print(f"[bold][Step {step_num}/9] {description}...[/bold]")
236
+
237
+ # 1. Load Prompt
238
+ template_name = f"agentic_e2e_fix_step{step_num}_{step_name}_LLM"
239
+ prompt_template = load_prompt_template(template_name)
240
+ if not prompt_template:
241
+ raise ValueError(f"Could not load prompt template: {template_name}")
242
+
243
+ # 2. Prepare Context
244
+ context = {
245
+ "issue_url": issue_url,
246
+ "repo_owner": repo_owner,
247
+ "repo_name": repo_name,
248
+ "issue_number": issue_number,
249
+ "cycle_number": current_cycle,
250
+ "max_cycles": max_cycles,
251
+ "issue_content": issue_content,
252
+ }
253
+
254
+ # Add previous step outputs
255
+ for prev_step in range(1, step_num):
256
+ key = f"step{prev_step}_output"
257
+ context[key] = step_outputs.get(str(prev_step), "")
258
+
259
+ # Derived variables for specific steps
260
+ if step_num >= 6:
261
+ s5_out = step_outputs.get("5", "")
262
+ context["dev_units_identified"] = _parse_dev_units(s5_out)
263
+
264
+ if step_num == 8:
265
+ s5_out = step_outputs.get("5", "")
266
+ context["failing_dev_units"] = _parse_dev_units(s5_out)
267
+
268
+ if step_num == 9:
269
+ context["next_cycle"] = current_cycle + 1
270
+
271
+ formatted_prompt = prompt_template.format(**context)
272
+
273
+ # 3. Run Task
274
+ base_timeout = E2E_FIX_STEP_TIMEOUTS.get(step_num, 340.0)
275
+ timeout = base_timeout + timeout_adder
276
+
277
+ step_success, step_output, step_cost, step_model = run_agentic_task(
278
+ instruction=formatted_prompt,
279
+ cwd=cwd,
280
+ verbose=verbose,
281
+ quiet=quiet,
282
+ timeout=timeout,
283
+ label=f"cycle{current_cycle}_step{step_num}"
284
+ )
285
+
286
+ # 4. Store Output & Accumulate
287
+ step_outputs[str(step_num)] = step_output
288
+ total_cost += step_cost
289
+ model_used = step_model if step_model else model_used
290
+
291
+ # Parse changed files
292
+ new_files = _parse_changed_files(step_output)
293
+ for f in new_files:
294
+ if f not in changed_files:
295
+ changed_files.append(f)
296
+
297
+ # Parse dev unit states (Step 8)
298
+ if step_num == 8:
299
+ s5_out = step_outputs.get("5", "")
300
+ dev_units_str = _parse_dev_units(s5_out)
301
+ dev_unit_states = _update_dev_unit_states(step_output, dev_unit_states, dev_units_str)
302
+
303
+ # Print brief result
304
+ console.print(f" -> Step {step_num} complete. Cost: ${step_cost:.4f}")
305
+
306
+ # 5. Save State
307
+ last_completed_step = step_num
308
+ state_data = {
309
+ "workflow": workflow_name,
310
+ "issue_url": issue_url,
311
+ "issue_number": issue_number,
312
+ "current_cycle": current_cycle,
313
+ "last_completed_step": last_completed_step,
314
+ "step_outputs": step_outputs,
315
+ "dev_unit_states": dev_unit_states,
316
+ "total_cost": total_cost,
317
+ "model_used": model_used,
318
+ "changed_files": changed_files,
319
+ "last_saved_at": datetime.now().isoformat(),
320
+ "github_comment_id": github_comment_id
321
+ }
322
+
323
+ new_gh_id = save_workflow_state(
324
+ cwd, issue_number, workflow_name, state_data, state_dir, repo_owner, repo_name, use_github_state, github_comment_id
325
+ )
326
+ if new_gh_id:
327
+ github_comment_id = new_gh_id
328
+
329
+ # Check Early Exit (Step 2)
330
+ if step_num == 2 and "ALL_TESTS_PASS" in step_output:
331
+ console.print("[green]ALL_TESTS_PASS detected in Step 2. Exiting loop.[/green]")
332
+ success = True
333
+ final_message = "All tests passed during e2e check."
334
+ break
335
+
336
+ # Check Loop Control (Step 9)
337
+ if step_num == 9:
338
+ if "ALL_TESTS_PASS" in step_output:
339
+ console.print("[green]ALL_TESTS_PASS detected in Step 9.[/green]")
340
+ success = True
341
+ final_message = "All tests passed after fixes."
342
+ break
343
+ elif "MAX_CYCLES_REACHED" in step_output:
344
+ console.print("[yellow]MAX_CYCLES_REACHED detected in Step 9.[/yellow]")
345
+ elif "CONTINUE_CYCLE" not in step_output:
346
+ console.print("[yellow]Warning: No loop control token found in Step 9. Defaulting to CONTINUE_CYCLE.[/yellow]")
347
+
348
+ if success:
349
+ break
350
+
351
+ # Prepare for next cycle
352
+ current_cycle += 1
353
+ last_completed_step = 0
354
+ step_outputs = {} # Clear outputs for next cycle
355
+
356
+ state_data["current_cycle"] = current_cycle
357
+ state_data["last_completed_step"] = 0
358
+ state_data["step_outputs"] = {}
359
+ state_data["last_saved_at"] = datetime.now().isoformat()
360
+
361
+ if current_cycle <= max_cycles:
362
+ save_workflow_state(
363
+ cwd, issue_number, workflow_name, state_data, state_dir, repo_owner, repo_name, use_github_state, github_comment_id
364
+ )
365
+
366
+ if success:
367
+ clear_workflow_state(cwd, issue_number, workflow_name, state_dir, repo_owner, repo_name, use_github_state)
368
+ console.print("\n[bold green]E2E fix complete[/bold green]")
369
+ console.print(f" Total cost: ${total_cost:.4f}")
370
+ console.print(f" Cycles used: {current_cycle if current_cycle <= max_cycles else max_cycles}/{max_cycles}")
371
+ console.print(f" Files changed: {', '.join(changed_files)}")
372
+ fixed_units = [u for u, s in dev_unit_states.items() if s.get("fixed")]
373
+ console.print(f" Dev units fixed: {', '.join(fixed_units)}")
374
+ return True, final_message, total_cost, model_used, changed_files
375
+ else:
376
+ final_message = f"Max cycles ({max_cycles}) reached without all tests passing"
377
+ console.print("\n[bold red]E2E fix incomplete (max cycles reached)[/bold red]")
378
+ console.print(f" Total cost: ${total_cost:.4f}")
379
+ remaining = [u for u, s in dev_unit_states.items() if not s.get("fixed")]
380
+ console.print(f" Remaining failures: {', '.join(remaining)}")
381
+ return False, final_message, total_cost, model_used, changed_files
382
+
383
+ except KeyboardInterrupt:
384
+ console.print("\n[bold red]Interrupted by user. Saving state...[/bold red]")
385
+ state_data = {
386
+ "workflow": workflow_name,
387
+ "issue_url": issue_url,
388
+ "issue_number": issue_number,
389
+ "current_cycle": current_cycle,
390
+ "last_completed_step": last_completed_step,
391
+ "step_outputs": step_outputs,
392
+ "dev_unit_states": dev_unit_states,
393
+ "total_cost": total_cost,
394
+ "model_used": model_used,
395
+ "changed_files": changed_files,
396
+ "last_saved_at": datetime.now().isoformat(),
397
+ "github_comment_id": github_comment_id
398
+ }
399
+ save_workflow_state(
400
+ cwd, issue_number, workflow_name, state_data, state_dir, repo_owner, repo_name, use_github_state, github_comment_id
401
+ )
402
+ raise
403
+
404
+ except Exception as e:
405
+ console.print(f"\n[bold red]Fatal error: {e}[/bold red]")
406
+ try:
407
+ state_data = {
408
+ "workflow": workflow_name,
409
+ "issue_url": issue_url,
410
+ "issue_number": issue_number,
411
+ "current_cycle": current_cycle,
412
+ "last_completed_step": last_completed_step,
413
+ "step_outputs": step_outputs,
414
+ "dev_unit_states": dev_unit_states,
415
+ "total_cost": total_cost,
416
+ "model_used": model_used,
417
+ "changed_files": changed_files,
418
+ "last_saved_at": datetime.now().isoformat(),
419
+ "github_comment_id": github_comment_id
420
+ }
421
+ save_workflow_state(
422
+ cwd, issue_number, workflow_name, state_data, state_dir, repo_owner, repo_name, use_github_state, github_comment_id
423
+ )
424
+ except Exception:
425
+ pass
426
+ return False, f"Stopped at cycle {current_cycle} step {last_completed_step}: {str(e)}", total_cost, model_used, changed_files