ctrlcode 0.1.0__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.
- ctrlcode/__init__.py +8 -0
- ctrlcode/agents/__init__.py +29 -0
- ctrlcode/agents/cleanup.py +388 -0
- ctrlcode/agents/communication.py +439 -0
- ctrlcode/agents/observability.py +421 -0
- ctrlcode/agents/react_loop.py +297 -0
- ctrlcode/agents/registry.py +211 -0
- ctrlcode/agents/result_parser.py +242 -0
- ctrlcode/agents/workflow.py +723 -0
- ctrlcode/analysis/__init__.py +28 -0
- ctrlcode/analysis/ast_diff.py +163 -0
- ctrlcode/analysis/bug_detector.py +149 -0
- ctrlcode/analysis/code_graphs.py +329 -0
- ctrlcode/analysis/semantic.py +205 -0
- ctrlcode/analysis/static.py +183 -0
- ctrlcode/analysis/synthesizer.py +281 -0
- ctrlcode/analysis/tests.py +189 -0
- ctrlcode/cleanup/__init__.py +16 -0
- ctrlcode/cleanup/auto_merge.py +350 -0
- ctrlcode/cleanup/doc_gardening.py +388 -0
- ctrlcode/cleanup/pr_automation.py +330 -0
- ctrlcode/cleanup/scheduler.py +356 -0
- ctrlcode/config.py +380 -0
- ctrlcode/embeddings/__init__.py +6 -0
- ctrlcode/embeddings/embedder.py +192 -0
- ctrlcode/embeddings/vector_store.py +213 -0
- ctrlcode/fuzzing/__init__.py +24 -0
- ctrlcode/fuzzing/analyzer.py +280 -0
- ctrlcode/fuzzing/budget.py +112 -0
- ctrlcode/fuzzing/context.py +665 -0
- ctrlcode/fuzzing/context_fuzzer.py +506 -0
- ctrlcode/fuzzing/derived_orchestrator.py +732 -0
- ctrlcode/fuzzing/oracle_adapter.py +135 -0
- ctrlcode/linters/__init__.py +11 -0
- ctrlcode/linters/hand_rolled_utils.py +221 -0
- ctrlcode/linters/yolo_parsing.py +217 -0
- ctrlcode/metrics/__init__.py +6 -0
- ctrlcode/metrics/dashboard.py +283 -0
- ctrlcode/metrics/tech_debt.py +663 -0
- ctrlcode/paths.py +68 -0
- ctrlcode/permissions.py +179 -0
- ctrlcode/providers/__init__.py +15 -0
- ctrlcode/providers/anthropic.py +138 -0
- ctrlcode/providers/base.py +77 -0
- ctrlcode/providers/openai.py +197 -0
- ctrlcode/providers/parallel.py +104 -0
- ctrlcode/server.py +871 -0
- ctrlcode/session/__init__.py +6 -0
- ctrlcode/session/baseline.py +57 -0
- ctrlcode/session/manager.py +967 -0
- ctrlcode/skills/__init__.py +10 -0
- ctrlcode/skills/builtin/commit.toml +29 -0
- ctrlcode/skills/builtin/docs.toml +25 -0
- ctrlcode/skills/builtin/refactor.toml +33 -0
- ctrlcode/skills/builtin/review.toml +28 -0
- ctrlcode/skills/builtin/test.toml +28 -0
- ctrlcode/skills/loader.py +111 -0
- ctrlcode/skills/registry.py +139 -0
- ctrlcode/storage/__init__.py +19 -0
- ctrlcode/storage/history_db.py +708 -0
- ctrlcode/tools/__init__.py +220 -0
- ctrlcode/tools/bash.py +112 -0
- ctrlcode/tools/browser.py +352 -0
- ctrlcode/tools/executor.py +153 -0
- ctrlcode/tools/explore.py +486 -0
- ctrlcode/tools/mcp.py +108 -0
- ctrlcode/tools/observability.py +561 -0
- ctrlcode/tools/registry.py +193 -0
- ctrlcode/tools/todo.py +291 -0
- ctrlcode/tools/update.py +266 -0
- ctrlcode/tools/webfetch.py +147 -0
- ctrlcode-0.1.0.dist-info/METADATA +93 -0
- ctrlcode-0.1.0.dist-info/RECORD +75 -0
- ctrlcode-0.1.0.dist-info/WHEEL +4 -0
- ctrlcode-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Parse agent execution results into structured outputs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentResultParser:
|
|
12
|
+
"""Parse outputs from different agent types."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def parse_planner_result(assistant_text: str, tool_calls: list[dict]) -> dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Extract task graph from planner output.
|
|
18
|
+
|
|
19
|
+
Priority:
|
|
20
|
+
1. task_write tool call with JSON task graph
|
|
21
|
+
2. JSON blocks in assistant text
|
|
22
|
+
3. Fallback: error with raw text
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
assistant_text: Full text response from planner
|
|
26
|
+
tool_calls: All tool calls made by planner
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Task graph dictionary
|
|
30
|
+
"""
|
|
31
|
+
# Priority 1: Look for task_write tool call
|
|
32
|
+
for tool_call in tool_calls:
|
|
33
|
+
if tool_call.get("tool") == "task_write":
|
|
34
|
+
tool_input = tool_call.get("input", {})
|
|
35
|
+
|
|
36
|
+
# Check if input already has task graph structure
|
|
37
|
+
if "tasks" in tool_input:
|
|
38
|
+
logger.info("Parsed task graph from task_write tool call (direct)")
|
|
39
|
+
return tool_input
|
|
40
|
+
|
|
41
|
+
# Otherwise try to parse from content field
|
|
42
|
+
content = tool_input.get("content", "")
|
|
43
|
+
if content:
|
|
44
|
+
try:
|
|
45
|
+
task_graph = json.loads(content)
|
|
46
|
+
logger.info("Parsed task graph from task_write tool call")
|
|
47
|
+
return task_graph
|
|
48
|
+
except json.JSONDecodeError as e:
|
|
49
|
+
logger.warning(f"Failed to parse task_write content: {e}")
|
|
50
|
+
|
|
51
|
+
# Priority 2: Extract JSON from text (look for code blocks or raw JSON)
|
|
52
|
+
json_blocks = re.findall(r'```(?:json)?\s*(\{.*?\})\s*```', assistant_text, re.DOTALL)
|
|
53
|
+
for block in json_blocks:
|
|
54
|
+
try:
|
|
55
|
+
task_graph = json.loads(block)
|
|
56
|
+
logger.info("Parsed task graph from JSON code block")
|
|
57
|
+
return task_graph
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Try raw JSON in text
|
|
62
|
+
try:
|
|
63
|
+
# Look for first { to last }
|
|
64
|
+
start = assistant_text.find('{')
|
|
65
|
+
end = assistant_text.rfind('}')
|
|
66
|
+
if start != -1 and end != -1:
|
|
67
|
+
potential_json = assistant_text[start:end + 1]
|
|
68
|
+
task_graph = json.loads(potential_json)
|
|
69
|
+
logger.info("Parsed task graph from raw JSON in text")
|
|
70
|
+
return task_graph
|
|
71
|
+
except (json.JSONDecodeError, ValueError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
# Priority 3: Fallback - return error
|
|
75
|
+
logger.error("Failed to parse task graph from planner output")
|
|
76
|
+
return {
|
|
77
|
+
"tasks": [],
|
|
78
|
+
"parallel_groups": [],
|
|
79
|
+
"risks": [],
|
|
80
|
+
"checkpoints": [],
|
|
81
|
+
"error": "Failed to parse task graph",
|
|
82
|
+
"raw_text": assistant_text[:500] # First 500 chars for debugging
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def parse_coder_result(assistant_text: str, tool_calls: list[dict]) -> dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Extract files changed and implementation details from coder output.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
assistant_text: Full text response from coder
|
|
92
|
+
tool_calls: All tool calls made by coder
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Coder result dictionary
|
|
96
|
+
"""
|
|
97
|
+
# Extract file operations from tool calls
|
|
98
|
+
files_written = []
|
|
99
|
+
files_edited = []
|
|
100
|
+
files_read = []
|
|
101
|
+
|
|
102
|
+
for tool_call in tool_calls:
|
|
103
|
+
tool_name = tool_call.get("tool", "")
|
|
104
|
+
tool_input = tool_call.get("input", {})
|
|
105
|
+
|
|
106
|
+
if tool_name == "write_file":
|
|
107
|
+
files_written.append(tool_input.get("path", "unknown"))
|
|
108
|
+
elif tool_name == "edit_file":
|
|
109
|
+
files_edited.append(tool_input.get("path", "unknown"))
|
|
110
|
+
elif tool_name == "read_file":
|
|
111
|
+
files_read.append(tool_input.get("path", "unknown"))
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"files_written": files_written,
|
|
115
|
+
"files_edited": files_edited,
|
|
116
|
+
"files_read": files_read,
|
|
117
|
+
"summary": assistant_text,
|
|
118
|
+
"tool_count": len(tool_calls)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def parse_reviewer_result(assistant_text: str, tool_calls: list[dict]) -> dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Extract approval/feedback from reviewer output.
|
|
125
|
+
|
|
126
|
+
Looks for:
|
|
127
|
+
- task_update with approval status
|
|
128
|
+
- Explicit approval/changes_requested keywords
|
|
129
|
+
- Feedback items
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
assistant_text: Full text response from reviewer
|
|
133
|
+
tool_calls: All tool calls made by reviewer
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Reviewer result dictionary
|
|
137
|
+
"""
|
|
138
|
+
# Look for task_update tool call
|
|
139
|
+
for tool_call in tool_calls:
|
|
140
|
+
if tool_call.get("tool") == "task_update":
|
|
141
|
+
tool_input = tool_call.get("input", {})
|
|
142
|
+
status = tool_input.get("status", "")
|
|
143
|
+
|
|
144
|
+
if status == "completed":
|
|
145
|
+
return {
|
|
146
|
+
"status": "approved",
|
|
147
|
+
"feedback": assistant_text,
|
|
148
|
+
"changes_required": []
|
|
149
|
+
}
|
|
150
|
+
elif "changes" in status.lower():
|
|
151
|
+
return {
|
|
152
|
+
"status": "changes_requested",
|
|
153
|
+
"feedback": assistant_text,
|
|
154
|
+
"changes_required": _extract_feedback_items(assistant_text)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Parse from text
|
|
158
|
+
text_lower = assistant_text.lower()
|
|
159
|
+
if any(keyword in text_lower for keyword in ["approved", "looks good", "lgtm"]):
|
|
160
|
+
return {
|
|
161
|
+
"status": "approved",
|
|
162
|
+
"feedback": assistant_text,
|
|
163
|
+
"changes_required": []
|
|
164
|
+
}
|
|
165
|
+
elif any(keyword in text_lower for keyword in ["changes requested", "needs work", "issues found"]):
|
|
166
|
+
return {
|
|
167
|
+
"status": "changes_requested",
|
|
168
|
+
"feedback": assistant_text,
|
|
169
|
+
"changes_required": _extract_feedback_items(assistant_text)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Default: inconclusive
|
|
173
|
+
return {
|
|
174
|
+
"status": "inconclusive",
|
|
175
|
+
"feedback": assistant_text,
|
|
176
|
+
"changes_required": []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def parse_executor_result(assistant_text: str, tool_calls: list[dict]) -> dict[str, Any]:
|
|
181
|
+
"""
|
|
182
|
+
Extract validation status and test results from executor output.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
assistant_text: Full text response from executor
|
|
186
|
+
tool_calls: All tool calls made by executor
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Executor result dictionary
|
|
190
|
+
"""
|
|
191
|
+
# Look for bash/test tool calls
|
|
192
|
+
test_results = []
|
|
193
|
+
commands_run = []
|
|
194
|
+
|
|
195
|
+
for tool_call in tool_calls:
|
|
196
|
+
tool_name = tool_call.get("tool", "")
|
|
197
|
+
tool_input = tool_call.get("input", {})
|
|
198
|
+
|
|
199
|
+
if tool_name == "bash":
|
|
200
|
+
command = tool_input.get("command", "")
|
|
201
|
+
commands_run.append(command)
|
|
202
|
+
|
|
203
|
+
# Detect test commands
|
|
204
|
+
if any(test_cmd in command for test_cmd in ["pytest", "npm test", "go test", "cargo test"]):
|
|
205
|
+
test_results.append({
|
|
206
|
+
"command": command,
|
|
207
|
+
"type": "test"
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
# Parse validation status from text
|
|
211
|
+
text_lower = assistant_text.lower()
|
|
212
|
+
if any(keyword in text_lower for keyword in ["all tests pass", "validation successful", "✓"]):
|
|
213
|
+
validation_status = "passed"
|
|
214
|
+
elif any(keyword in text_lower for keyword in ["tests fail", "validation failed", "✗", "error"]):
|
|
215
|
+
validation_status = "failed"
|
|
216
|
+
else:
|
|
217
|
+
validation_status = "inconclusive"
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"validation_status": validation_status,
|
|
221
|
+
"test_results": test_results,
|
|
222
|
+
"commands_run": commands_run,
|
|
223
|
+
"output": assistant_text
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _extract_feedback_items(text: str) -> list[str]:
|
|
228
|
+
"""Extract bulleted or numbered feedback items from text."""
|
|
229
|
+
items = []
|
|
230
|
+
|
|
231
|
+
# Look for markdown lists (- or 1.)
|
|
232
|
+
lines = text.split('\n')
|
|
233
|
+
for line in lines:
|
|
234
|
+
stripped = line.strip()
|
|
235
|
+
# Bullet points
|
|
236
|
+
if stripped.startswith('- ') or stripped.startswith('* '):
|
|
237
|
+
items.append(stripped[2:].strip())
|
|
238
|
+
# Numbered lists
|
|
239
|
+
elif re.match(r'^\d+\.\s+', stripped):
|
|
240
|
+
items.append(re.sub(r'^\d+\.\s+', '', stripped))
|
|
241
|
+
|
|
242
|
+
return items
|