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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- 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()
|