yamlgraph 0.3.9__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 (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
@@ -0,0 +1,614 @@
1
+ """Impl-agent instruction executor.
2
+
3
+ Parses impl-agent output and generates shell scripts for review.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import re
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def parse_instruction(instruction: str) -> dict[str, Any]:
18
+ """Parse a single impl-agent instruction into structured data.
19
+
20
+ Args:
21
+ instruction: Raw instruction string like "EXTRACT function name (lines 70-92) from x.py → y.py"
22
+
23
+ Returns:
24
+ Dict with action, details, and original instruction
25
+ """
26
+ result = {"original": instruction, "action": None, "valid": False}
27
+
28
+ # Clean up: remove backticks from file paths
29
+ clean_instr = re.sub(r"`([^`]+)`", r"\1", instruction)
30
+
31
+ # EXTRACT single function: EXTRACT function name (lines X-Y) from file.py → new_file.py
32
+ extract_single = re.match(
33
+ r"EXTRACT\s+function\s+(\w+)\s+\(lines?\s+(\d+)-(\d+)\)\s+from\s+([^\s→]+)\s*→\s*(.+)",
34
+ clean_instr,
35
+ re.IGNORECASE,
36
+ )
37
+ if extract_single:
38
+ return {
39
+ "original": instruction,
40
+ "action": "EXTRACT",
41
+ "functions": [extract_single.group(1).strip()],
42
+ "start_line": int(extract_single.group(2)),
43
+ "end_line": int(extract_single.group(3)),
44
+ "source_file": extract_single.group(4).strip().rstrip("."),
45
+ "target_file": extract_single.group(5).strip().rstrip("."),
46
+ "valid": True,
47
+ }
48
+
49
+ # EXTRACT multiple functions (legacy): EXTRACT functions [name1, name2] (lines X-Y) from file.py → new_file.py
50
+ extract_match = re.match(
51
+ r"EXTRACT\s+functions?\s+\[([^\]]+)\]\s+\(lines?\s+(\d+)-(\d+)\)\s+from\s+([^\s→]+)\s*→\s*(.+)",
52
+ clean_instr,
53
+ re.IGNORECASE,
54
+ )
55
+ if extract_match:
56
+ funcs = [f.strip().strip("'\"") for f in extract_match.group(1).split(",")]
57
+ return {
58
+ "original": instruction,
59
+ "action": "EXTRACT",
60
+ "functions": funcs,
61
+ "start_line": int(extract_match.group(2)),
62
+ "end_line": int(extract_match.group(3)),
63
+ "source_file": extract_match.group(4).strip().rstrip("."),
64
+ "target_file": extract_match.group(5).strip().rstrip("."),
65
+ "valid": True,
66
+ }
67
+
68
+ # DELETE single function: DELETE function name (lines X-Y) in file.py (reason)
69
+ delete_func = re.match(
70
+ r"DELETE\s+function\s+(\w+)\s+\(lines?\s+(\d+)-(\d+)\)\s+in\s+([^\s(]+)(?:\s*\(([^)]+)\))?",
71
+ clean_instr,
72
+ re.IGNORECASE,
73
+ )
74
+ if delete_func:
75
+ return {
76
+ "original": instruction,
77
+ "action": "DELETE",
78
+ "function": delete_func.group(1).strip(),
79
+ "start_line": int(delete_func.group(2)),
80
+ "end_line": int(delete_func.group(3)),
81
+ "file": delete_func.group(4).strip().rstrip("."),
82
+ "reason": delete_func.group(5) if delete_func.group(5) else "",
83
+ "valid": True,
84
+ }
85
+
86
+ # DELETE lines (legacy): DELETE lines X-Y in file.py (reason)
87
+ delete_match = re.match(
88
+ r"DELETE\s+lines?\s+(\d+)-(\d+)\s+in\s+([^\s(]+)(?:\s*\(([^)]+)\))?",
89
+ clean_instr,
90
+ re.IGNORECASE,
91
+ )
92
+ if delete_match:
93
+ return {
94
+ "original": instruction,
95
+ "action": "DELETE",
96
+ "start_line": int(delete_match.group(1)),
97
+ "end_line": int(delete_match.group(2)),
98
+ "file": delete_match.group(3).strip().rstrip("."),
99
+ "reason": delete_match.group(4) if delete_match.group(4) else "",
100
+ "valid": True,
101
+ }
102
+
103
+ # ADD pattern: ADD import/statement at line X in file.py: 'content'
104
+ add_match = re.match(
105
+ r"ADD\s+(.+?)\s+at\s+line\s+(\d+)\s+in\s+([^\s:]+):\s*['\"`]?(.+?)['\"`]?\s*$",
106
+ clean_instr,
107
+ re.IGNORECASE | re.DOTALL,
108
+ )
109
+ if add_match:
110
+ # Clean content: strip backticks and trailing periods
111
+ content = add_match.group(4).strip()
112
+ content = content.strip("`'\"").rstrip(".")
113
+ return {
114
+ "original": instruction,
115
+ "action": "ADD",
116
+ "what": add_match.group(1).strip(),
117
+ "line": int(add_match.group(2)),
118
+ "file": add_match.group(3).strip().rstrip("."),
119
+ "content": content,
120
+ "valid": True,
121
+ }
122
+
123
+ # ADD after line pattern: ADD ... after line X in file.py
124
+ add_after_match = re.match(
125
+ r"ADD\s+(.+?)\s+after\s+line\s+(\d+)\s+in\s+([^\s:]+)",
126
+ clean_instr,
127
+ re.IGNORECASE,
128
+ )
129
+ if add_after_match:
130
+ return {
131
+ "original": instruction,
132
+ "action": "ADD_AFTER",
133
+ "what": add_after_match.group(1).strip(),
134
+ "after_line": int(add_after_match.group(2)),
135
+ "file": add_after_match.group(3).strip().rstrip("."),
136
+ "valid": True,
137
+ }
138
+
139
+ # CREATE pattern: CREATE file.py with ...
140
+ create_match = re.match(
141
+ r"CREATE\s+([^\s]+)\s+with\s+(.+)",
142
+ clean_instr,
143
+ re.IGNORECASE | re.DOTALL,
144
+ )
145
+ if create_match:
146
+ return {
147
+ "original": instruction,
148
+ "action": "CREATE",
149
+ "file": create_match.group(1).strip().rstrip("."),
150
+ "content_desc": create_match.group(2).strip(),
151
+ "valid": True,
152
+ }
153
+
154
+ # MODIFY pattern: MODIFY function_name (lines X-Y) in file.py: description
155
+ modify_match = re.match(
156
+ r"MODIFY\s+([^\s(]+)\s+\(lines?\s+(\d+)-(\d+)\)\s+in\s+([^\s:]+):\s*(.+)",
157
+ clean_instr,
158
+ re.IGNORECASE | re.DOTALL,
159
+ )
160
+ if modify_match:
161
+ return {
162
+ "original": instruction,
163
+ "action": "MODIFY",
164
+ "function": modify_match.group(1).strip(),
165
+ "start_line": int(modify_match.group(2)),
166
+ "end_line": int(modify_match.group(3)),
167
+ "file": modify_match.group(4).strip().rstrip("."),
168
+ "description": modify_match.group(5).strip(),
169
+ "valid": True,
170
+ }
171
+
172
+ # MODIFY without line numbers
173
+ modify_simple = re.match(
174
+ r"MODIFY\s+([^\s]+)\s+to\s+(.+)",
175
+ clean_instr,
176
+ re.IGNORECASE | re.DOTALL,
177
+ )
178
+ if modify_simple:
179
+ return {
180
+ "original": instruction,
181
+ "action": "MODIFY_SIMPLE",
182
+ "target": modify_simple.group(1).strip(),
183
+ "description": modify_simple.group(2).strip(),
184
+ "valid": True,
185
+ }
186
+
187
+ # VERIFY pattern
188
+ if instruction.upper().startswith("VERIFY"):
189
+ return {
190
+ "original": instruction,
191
+ "action": "VERIFY",
192
+ "description": instruction[6:].strip(),
193
+ "valid": True,
194
+ }
195
+
196
+ # Unrecognized - still include as comment
197
+ return result
198
+
199
+
200
+ def instruction_to_shell(parsed: dict[str, Any], project_root: str = ".") -> str:
201
+ """Convert a parsed instruction to shell commands.
202
+
203
+ Args:
204
+ parsed: Parsed instruction dict from parse_instruction()
205
+ project_root: Root directory for file paths
206
+
207
+ Returns:
208
+ Shell script snippet with comments and commands
209
+ """
210
+ lines = []
211
+ lines.append(f"\n# {'-' * 70}")
212
+
213
+ # Handle multi-line instructions: convert newlines to comment continuations
214
+ original = parsed["original"]
215
+ instr_lines = original.split("\n")
216
+ first_line = instr_lines[0][:100]
217
+ lines.append(f"# INSTRUCTION: {first_line}")
218
+
219
+ # Add continuation for long first line
220
+ if len(instr_lines[0]) > 100:
221
+ lines.append(f"# {instr_lines[0][100:200]}")
222
+
223
+ # Add remaining lines as comments (truncate at 5 lines for brevity)
224
+ for extra_line in instr_lines[1:5]:
225
+ lines.append(f"# {extra_line[:100]}")
226
+ if len(instr_lines) > 5:
227
+ lines.append(f"# ... ({len(instr_lines) - 5} more lines)")
228
+
229
+ lines.append(f"# {'-' * 70}")
230
+
231
+ if not parsed.get("valid"):
232
+ lines.append("# ⚠️ Could not parse this instruction - manual review required")
233
+ lines.append(f'echo "MANUAL: {first_line[:60]}..."')
234
+ return "\n".join(lines)
235
+
236
+ action = parsed["action"]
237
+
238
+ if action == "EXTRACT":
239
+ src = f"{project_root}/{parsed['source_file']}"
240
+ tgt = f"{project_root}/{parsed['target_file']}"
241
+ start = parsed["start_line"]
242
+ end = parsed["end_line"]
243
+ funcs = ", ".join(parsed["functions"])
244
+
245
+ lines.append(f"# Extracting: {funcs}")
246
+ lines.append(f"# From: {src} (lines {start}-{end})")
247
+ lines.append(f"# To: {tgt}")
248
+ lines.append("")
249
+ lines.append(f"# Append lines {start}-{end} from source to target")
250
+ lines.append(f'sed -n \'{start},{end}p\' "{src}" >> "{tgt}"')
251
+ lines.append("")
252
+
253
+ elif action == "DELETE":
254
+ file = f"{project_root}/{parsed['file']}"
255
+ start = parsed["start_line"]
256
+ end = parsed["end_line"]
257
+ reason = parsed.get("reason", "")
258
+
259
+ lines.append(
260
+ f"# Delete lines {start}-{end}" + (f" ({reason})" if reason else "")
261
+ )
262
+ lines.append("# Creating backup first")
263
+ lines.append(f'cp "{file}" "{file}.bak"')
264
+ lines.append(f"# Delete lines {start} to {end}")
265
+ lines.append(f"sed -i '' '{start},{end}d' \"{file}\"")
266
+ lines.append("")
267
+
268
+ elif action == "ADD":
269
+ file = f"{project_root}/{parsed['file']}"
270
+ line = parsed["line"]
271
+ content = parsed["content"].replace("'", "'\"'\"'") # Escape single quotes
272
+
273
+ lines.append(f"# Add content at line {line}")
274
+ lines.append(f"# Content: {parsed['content'][:50]}...")
275
+ lines.append(f"sed -i '' '{line}i\\")
276
+ lines.append(f"{content}")
277
+ lines.append(f'\' "{file}"')
278
+ lines.append("")
279
+
280
+ elif action == "ADD_AFTER":
281
+ file = f"{project_root}/{parsed['file']}"
282
+ after = parsed["after_line"]
283
+
284
+ lines.append(f"# Add after line {after} in {file}")
285
+ lines.append(f"# What: {parsed['what']}")
286
+ lines.append(
287
+ f'echo "MANUAL: Add {parsed["what"]} after line {after} in {file}"'
288
+ )
289
+ lines.append("")
290
+
291
+ elif action == "CREATE":
292
+ file = f"{project_root}/{parsed['file']}"
293
+
294
+ lines.append(f"# Create new file: {file}")
295
+ lines.append(f"# Content: {parsed['content_desc'][:60]}...")
296
+ lines.append(f'mkdir -p "$(dirname "{file}")"')
297
+ lines.append(f'touch "{file}"')
298
+ lines.append(f'echo "CREATED: {file} - populate with extracted content"')
299
+ lines.append("")
300
+
301
+ elif action == "MODIFY":
302
+ file = f"{project_root}/{parsed['file']}"
303
+ start = parsed["start_line"]
304
+ end = parsed["end_line"]
305
+
306
+ lines.append(f"# Modify {parsed['function']} (lines {start}-{end})")
307
+ lines.append(f"# Change: {parsed['description'][:60]}...")
308
+ lines.append(f'echo "MANUAL MODIFY: {parsed["function"]} in {file}"')
309
+ lines.append(f'echo " Lines: {start}-{end}"')
310
+ lines.append(f'echo " Change: {parsed["description"][:60]}..."')
311
+ lines.append("")
312
+
313
+ elif action == "MODIFY_SIMPLE":
314
+ lines.append(f"# Modify: {parsed['target']}")
315
+ lines.append(f"# Description: {parsed['description'][:60]}...")
316
+ lines.append(f'echo "MANUAL: Modify {parsed["target"]}"')
317
+ lines.append("")
318
+
319
+ elif action == "VERIFY":
320
+ lines.append("# Verification step")
321
+ lines.append(f"echo 'VERIFY: {parsed['description'][:60]}...'")
322
+ lines.append("")
323
+
324
+ return "\n".join(lines)
325
+
326
+
327
+ def generate_refactor_script(
328
+ instructions: list[str],
329
+ output_path: str | None = None,
330
+ project_root: str = ".",
331
+ ) -> str:
332
+ """Generate a shell script from impl-agent instructions.
333
+
334
+ Args:
335
+ instructions: List of instruction strings from implementation_plan.instructions
336
+ output_path: Where to save the script (auto-generates if None)
337
+ project_root: Root directory for file paths
338
+
339
+ Returns:
340
+ Path to the generated script
341
+ """
342
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
343
+
344
+ if output_path is None:
345
+ output_dir = Path(project_root) / "outputs" / "scripts"
346
+ output_dir.mkdir(parents=True, exist_ok=True)
347
+ output_path = str(output_dir / f"refactor_{timestamp}.sh")
348
+
349
+ script_lines = [
350
+ "#!/bin/bash",
351
+ f"# Generated by impl-agent executor on {timestamp}",
352
+ "# Review carefully before running!",
353
+ "",
354
+ "set -e # Exit on error",
355
+ "",
356
+ f'PROJECT_ROOT="{project_root}"',
357
+ 'cd "$PROJECT_ROOT"',
358
+ "",
359
+ "# ==========================================================================",
360
+ f"# REFACTOR SCRIPT - {len(instructions)} instructions",
361
+ "# ==========================================================================",
362
+ "",
363
+ ]
364
+
365
+ # Parse all instructions first
366
+ parsed_instructions = []
367
+ for i, instr in enumerate(instructions, 1):
368
+ # Clean up instruction (remove markdown artifacts)
369
+ clean_instr = instr.strip()
370
+ if clean_instr.startswith("```"):
371
+ continue # Skip code blocks
372
+
373
+ parsed = parse_instruction(clean_instr)
374
+ parsed["index"] = i
375
+ parsed_instructions.append(parsed)
376
+
377
+ # CRITICAL: Reorder DELETEs to process highest line numbers first per file
378
+ # This prevents line number shifts from breaking subsequent deletes
379
+ delete_instructions = [
380
+ p for p in parsed_instructions if p.get("action") == "DELETE"
381
+ ]
382
+ non_delete_instructions = [
383
+ p for p in parsed_instructions if p.get("action") != "DELETE"
384
+ ]
385
+
386
+ # Sort DELETEs by (file, -start_line) so highest lines deleted first
387
+ delete_instructions.sort(
388
+ key=lambda x: (x.get("file", ""), -(x.get("start_line", 0)))
389
+ )
390
+
391
+ # Group: non-deletes first (in original order), then sorted deletes
392
+ reordered = non_delete_instructions + delete_instructions
393
+
394
+ # Add warning about reordering if we had deletes
395
+ if delete_instructions:
396
+ script_lines.append(
397
+ "# ⚠️ DELETE instructions reordered: highest line numbers first"
398
+ )
399
+ script_lines.append(
400
+ "# This prevents line shift issues during multi-delete operations"
401
+ )
402
+ script_lines.append("")
403
+
404
+ # Convert to shell commands with new step numbers
405
+ for i, parsed in enumerate(reordered, 1):
406
+ script_lines.append(f"# Step {i}/{len(reordered)}")
407
+ script_lines.append(instruction_to_shell(parsed, "$PROJECT_ROOT"))
408
+
409
+ # Add summary
410
+ valid_count = sum(1 for p in reordered if p.get("valid"))
411
+ manual_count = len(reordered) - valid_count
412
+
413
+ script_lines.extend(
414
+ [
415
+ "",
416
+ "# ==========================================================================",
417
+ "# SUMMARY",
418
+ "# ==========================================================================",
419
+ f"# Total instructions: {len(instructions)}",
420
+ f"# Automated: {valid_count}",
421
+ f"# Manual review needed: {manual_count}",
422
+ "",
423
+ 'echo ""',
424
+ 'echo "Refactor script completed."',
425
+ f'echo " {valid_count} automated steps"',
426
+ f'echo " {manual_count} manual steps (marked with MANUAL:)"',
427
+ "",
428
+ ]
429
+ )
430
+
431
+ # Write script
432
+ script_content = "\n".join(script_lines)
433
+ Path(output_path).write_text(script_content)
434
+ Path(output_path).chmod(0o755) # Make executable
435
+
436
+ # Generate companion tasks file for manual/LLM handling
437
+ tasks_path = output_path.replace(".sh", "_tasks.md")
438
+ tasks_content = generate_tasks_file(reordered, project_root, timestamp)
439
+ Path(tasks_path).write_text(tasks_content)
440
+
441
+ logger.info(f"Generated refactor script: {output_path}")
442
+ logger.info(f"Generated tasks file: {tasks_path}")
443
+ return output_path
444
+
445
+
446
+ def generate_tasks_file(
447
+ parsed_instructions: list[dict[str, Any]],
448
+ project_root: str,
449
+ timestamp: str,
450
+ ) -> str:
451
+ """Generate a markdown file with full task details for manual/LLM handling.
452
+
453
+ Args:
454
+ parsed_instructions: List of parsed instruction dicts (already reordered)
455
+ project_root: Root directory for file paths
456
+ timestamp: Timestamp for the file header
457
+
458
+ Returns:
459
+ Markdown content as string
460
+ """
461
+ lines = [
462
+ f"# Refactor Tasks - {timestamp}",
463
+ "",
464
+ "Tasks for manual review or LLM execution. Full instruction text preserved.",
465
+ "",
466
+ "---",
467
+ "",
468
+ ]
469
+
470
+ # Separate automated vs manual tasks
471
+ automated = [p for p in parsed_instructions if p.get("valid")]
472
+ manual = [p for p in parsed_instructions if not p.get("valid")]
473
+
474
+ # Summary
475
+ lines.extend(
476
+ [
477
+ "## Summary",
478
+ "",
479
+ f"- **Total tasks**: {len(parsed_instructions)}",
480
+ f"- **Automated** (in shell script): {len(automated)}",
481
+ f"- **Manual/LLM** (below): {len(manual)}",
482
+ "",
483
+ "---",
484
+ "",
485
+ ]
486
+ )
487
+
488
+ # Manual tasks section (full detail)
489
+ if manual:
490
+ lines.extend(
491
+ [
492
+ "## Manual Tasks",
493
+ "",
494
+ "These tasks require manual implementation or LLM assistance.",
495
+ "",
496
+ ]
497
+ )
498
+
499
+ for i, task in enumerate(manual, 1):
500
+ # Extract action keyword from instruction start
501
+ original = task.get("original", "")
502
+ action = task.get("action")
503
+ if not action:
504
+ # Try to extract from first word
505
+ first_word = original.split()[0].upper() if original else "UNKNOWN"
506
+ action = first_word.rstrip(":")
507
+ lines.append(f"### Task {i}: {action}")
508
+ lines.append("")
509
+ lines.append("**Full Instruction:**")
510
+ lines.append("```")
511
+ lines.append(task["original"])
512
+ lines.append("```")
513
+ lines.append("")
514
+
515
+ # Add helpful context based on action type
516
+ if "file" in task:
517
+ lines.append(f"**Target file**: `{project_root}/{task['file']}`")
518
+ if "line" in task:
519
+ lines.append(f"**At line**: {task['line']}")
520
+ lines.append("")
521
+ lines.append("---")
522
+ lines.append("")
523
+
524
+ # Automated tasks reference (brief)
525
+ if automated:
526
+ lines.extend(
527
+ [
528
+ "## Automated Tasks (Reference)",
529
+ "",
530
+ "These are handled by the shell script. Listed here for completeness.",
531
+ "",
532
+ "| # | Action | Target | Lines |",
533
+ "|---|--------|--------|-------|",
534
+ ]
535
+ )
536
+
537
+ for i, task in enumerate(automated, 1):
538
+ action = task.get("action", "?")
539
+ target = task.get("file", task.get("source_file", "?"))
540
+ if len(target) > 40:
541
+ target = "..." + target[-37:]
542
+ start = task.get("start_line", "")
543
+ end = task.get("end_line", "")
544
+ line_range = f"{start}-{end}" if start and end else ""
545
+ lines.append(f"| {i} | {action} | `{target}` | {line_range} |")
546
+
547
+ lines.append("")
548
+
549
+ return "\n".join(lines)
550
+
551
+
552
+ def extract_instructions_from_output(output_text: str) -> list[str]:
553
+ """Extract instructions list from impl-agent raw output.
554
+
555
+ Args:
556
+ output_text: Raw output from impl-agent run
557
+
558
+ Returns:
559
+ List of instruction strings
560
+ """
561
+ # Try to find implementation_plan.instructions in the output
562
+ # Format: instructions=['...', '...'] test_instructions=[...]
563
+
564
+ # Look for instructions= and capture until '] test_instructions=' or '] risks='
565
+ # Use non-greedy matching and explicit field boundary
566
+ match = re.search(
567
+ r"instructions=(\[.*?\])\s+(?:test_instructions=|risks=)",
568
+ output_text,
569
+ re.DOTALL,
570
+ )
571
+ if match:
572
+ # Parse the list content (remove outer brackets)
573
+ list_str = match.group(1)
574
+ # Split by quoted strings - handle escaped quotes
575
+ instructions = re.findall(r"'((?:[^'\\]|\\.)*)'", list_str)
576
+ if instructions:
577
+ # Unescape newlines and quotes
578
+ return [i.replace("\\n", "\n").replace("\\'", "'") for i in instructions]
579
+
580
+ # Alternative: look for numbered instructions
581
+ numbered = re.findall(r"^\d+\.\s*(.+)$", output_text, re.MULTILINE)
582
+ if numbered:
583
+ return numbered
584
+
585
+ return []
586
+
587
+
588
+ def main():
589
+ """CLI entry point for testing."""
590
+ import sys
591
+
592
+ if len(sys.argv) < 2:
593
+ print("Usage: python impl_executor.py <impl-agent-output.txt>")
594
+ sys.exit(1)
595
+
596
+ input_file = sys.argv[1]
597
+ output_text = Path(input_file).read_text()
598
+
599
+ instructions = extract_instructions_from_output(output_text)
600
+ if not instructions:
601
+ print("No instructions found in output")
602
+ sys.exit(1)
603
+
604
+ print(f"Found {len(instructions)} instructions")
605
+
606
+ script_path = generate_refactor_script(
607
+ instructions,
608
+ project_root=str(Path.cwd()),
609
+ )
610
+ print(f"Generated script: {script_path}")
611
+
612
+
613
+ if __name__ == "__main__":
614
+ main()