superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,778 @@
1
+ """
2
+ QE Orchestrator - High-level interface for running QE sessions.
3
+
4
+ Provides:
5
+ - Simple API for CLI and CI integration
6
+ - Pre-configured quick scan and deep QE modes
7
+ - Role-based execution (--role flag)
8
+ - Noise controls integration
9
+ - Output formatting for different contexts
10
+ """
11
+
12
+ import asyncio
13
+ import json
14
+ import os
15
+ import shutil
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional
19
+ import logging
20
+ import subprocess
21
+
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
25
+ from rich.table import Table
26
+ from rich.text import Text
27
+
28
+ from .session import QESession, QESessionConfig, QESessionResult
29
+ from .noise import NoiseFilter, NoiseConfig, load_noise_config, Finding as NoiseFinding
30
+ from .roles import get_role, list_roles, RoleResult, RoleType
31
+ from .verifier import FixVerifier, FixVerifierConfig, VerificationResult, VerificationStatus
32
+ from superqode.execution.modes import QEMode
33
+ from superqode.enterprise import require_enterprise
34
+ from superqode.workspace import prepare_qe_worktree, GitWorktreeManager, WorktreeInfo
35
+
36
+ logger = logging.getLogger(__name__)
37
+ console = Console()
38
+
39
+
40
+ class SuggestionMode:
41
+ """
42
+ Manages the suggestion workflow when allow_suggestions is enabled.
43
+
44
+ The workflow:
45
+ 1. Agent finds bug and suggests fix
46
+ 2. Fix is applied in sandbox
47
+ 3. Tests run to verify fix
48
+ 4. Results compared (before/after)
49
+ 5. Evidence collected for QIR
50
+ 6. Changes reverted (always)
51
+ 7. Patches preserved for user decision
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ project_root: Path,
57
+ verifier_config: Optional[FixVerifierConfig] = None,
58
+ ):
59
+ self.project_root = project_root
60
+ self.verifier = FixVerifier(project_root, verifier_config)
61
+ self.verified_fixes: List[VerificationResult] = []
62
+
63
+ def verify_finding_fix(
64
+ self,
65
+ finding: Dict[str, Any],
66
+ apply_fix_fn: Optional[Callable[[str], bool]] = None,
67
+ ) -> Optional[VerificationResult]:
68
+ """
69
+ Verify a suggested fix for a finding.
70
+
71
+ Args:
72
+ finding: Finding dict with suggested_fix
73
+ apply_fix_fn: Optional function to apply the fix
74
+
75
+ Returns:
76
+ VerificationResult if fix was verified, None if no fix available
77
+ """
78
+ suggested_fix = finding.get("suggested_fix")
79
+ if not suggested_fix:
80
+ return None
81
+
82
+ result = self.verifier.verify_fix(
83
+ finding_id=finding.get("id", "unknown"),
84
+ patch_content=suggested_fix,
85
+ target_file=Path(finding.get("file_path", "")) if finding.get("file_path") else None,
86
+ apply_patch_fn=apply_fix_fn,
87
+ )
88
+
89
+ self.verified_fixes.append(result)
90
+ return result
91
+
92
+ def get_summary(self) -> Dict[str, Any]:
93
+ """Get summary of all verified fixes."""
94
+ return {
95
+ "total": len(self.verified_fixes),
96
+ "verified": sum(
97
+ 1 for f in self.verified_fixes if f.status == VerificationStatus.PASSED
98
+ ),
99
+ "improvements": sum(1 for f in self.verified_fixes if f.is_improvement),
100
+ "failed": sum(1 for f in self.verified_fixes if f.status == VerificationStatus.FAILED),
101
+ }
102
+
103
+
104
+ class QEOrchestrator:
105
+ """
106
+ High-level orchestrator for QE sessions.
107
+
108
+ Usage:
109
+ orchestrator = QEOrchestrator(Path("."))
110
+
111
+ # Quick scan (pre-commit, fast CI)
112
+ result = await orchestrator.quick_scan()
113
+
114
+ # Deep QE (pre-release, nightly CI)
115
+ result = await orchestrator.deep_qe()
116
+
117
+ # Run specific roles
118
+ result = await orchestrator.run_roles(["api_tester", "security_tester"])
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ project_root: Path,
124
+ verbose: bool = False,
125
+ output_format: str = "rich", # "rich", "json", "jsonl", "plain"
126
+ use_worktree: bool = False,
127
+ allow_suggestions: bool = False, # Enable suggestion workflow
128
+ ):
129
+ self.project_root = project_root.resolve()
130
+ self.verbose = verbose
131
+ self.output_format = output_format
132
+ self.use_worktree = use_worktree
133
+ self.allow_suggestions = allow_suggestions
134
+ self._current_session: Optional[QESession] = None
135
+
136
+ # Load noise configuration
137
+ self.noise_config = load_noise_config(self.project_root)
138
+ self.noise_filter = NoiseFilter(self.noise_config)
139
+
140
+ # Suggestion mode (initialized when allow_suggestions is True)
141
+ self._suggestion_mode: Optional[SuggestionMode] = None
142
+ if allow_suggestions:
143
+ if require_enterprise("Fix suggestions and verification"):
144
+ self._suggestion_mode = SuggestionMode(self.project_root)
145
+ else:
146
+ self.allow_suggestions = False
147
+
148
+ async def quick_scan(
149
+ self,
150
+ on_progress: Optional[Callable[[str], None]] = None,
151
+ ) -> QESessionResult:
152
+ """
153
+ Run a quick scan QE session.
154
+
155
+ - Time-boxed (60 seconds default)
156
+ - Shallow exploration
157
+ - High-risk paths only
158
+ - Minimal QIR
159
+
160
+ Best for: Pre-commit, developer laptop, fast CI feedback
161
+ """
162
+ config = QESessionConfig.quick_scan()
163
+ return await self._run_session(config, on_progress)
164
+
165
+ async def deep_qe(
166
+ self,
167
+ on_progress: Optional[Callable[[str], None]] = None,
168
+ ) -> QESessionResult:
169
+ """
170
+ Run a deep QE session.
171
+
172
+ - Full sandbox
173
+ - Destructive testing allowed
174
+ - Failure simulation hooks
175
+ - Full Investigation Reports
176
+
177
+ Best for: Pre-release, nightly CI, compliance evidence
178
+ """
179
+ config = QESessionConfig.deep_qe()
180
+ config.verbose = self.verbose # Pass verbose flag from orchestrator
181
+
182
+ # Override agent roles with YAML configuration if specified
183
+ yaml_agent_roles = self._get_deep_analysis_roles_from_yaml()
184
+ if yaml_agent_roles:
185
+ config.agent_roles = yaml_agent_roles
186
+
187
+ return await self._run_session(config, on_progress)
188
+
189
+ def _get_deep_analysis_roles_from_yaml(self) -> Optional[List[str]]:
190
+ """
191
+ Get deep analysis roles from YAML configuration.
192
+
193
+ Returns:
194
+ List of role names if configured, None to use defaults.
195
+ Empty list means use all enabled QE roles that have implementations.
196
+ """
197
+ try:
198
+ from superqode.config import load_config
199
+ from superqode.superqe.roles import ROLE_REGISTRY, load_role_config_from_yaml, RoleType
200
+
201
+ config = load_config()
202
+
203
+ if (
204
+ hasattr(config, "team")
205
+ and config.team
206
+ and hasattr(config.team, "modes")
207
+ and config.team.modes
208
+ and "qe" in config.team.modes
209
+ ):
210
+ qe_mode = config.team.modes["qe"]
211
+ implemented_roles = set(ROLE_REGISTRY.keys())
212
+ execution_roles = {
213
+ name
214
+ for name in implemented_roles
215
+ if (load_role_config_from_yaml(name, self.project_root) or {}).get("role_type")
216
+ == RoleType.EXECUTION.value
217
+ }
218
+
219
+ if hasattr(qe_mode, "deep_analysis_roles") and qe_mode.deep_analysis_roles:
220
+ # Return configured roles that have implementations
221
+ configured_roles = [
222
+ role
223
+ for role in qe_mode.deep_analysis_roles
224
+ if role in implemented_roles and role not in execution_roles
225
+ ]
226
+ return configured_roles if configured_roles else None
227
+ elif hasattr(qe_mode, "roles") and qe_mode.roles:
228
+ # If deep_analysis_roles is empty but roles exist,
229
+ # return all enabled QE roles that have implementations
230
+ enabled_implemented_roles = []
231
+ for role_name, role_config in qe_mode.roles.items():
232
+ if (
233
+ getattr(role_config, "enabled", True)
234
+ and role_name in implemented_roles
235
+ and role_name not in execution_roles
236
+ ):
237
+ enabled_implemented_roles.append(role_name)
238
+ return enabled_implemented_roles if enabled_implemented_roles else None
239
+
240
+ except Exception as e:
241
+ logger.debug(f"Failed to load deep analysis roles from YAML: {e}")
242
+
243
+ return None # Use hardcoded defaults
244
+
245
+ async def run(
246
+ self,
247
+ config: Optional[QESessionConfig] = None,
248
+ on_progress: Optional[Callable[[str], None]] = None,
249
+ ) -> QESessionResult:
250
+ """Run a QE session with custom configuration."""
251
+ config = config or QESessionConfig()
252
+ config.verbose = self.verbose # Pass verbose flag from orchestrator
253
+ return await self._run_session(config, on_progress)
254
+
255
+ async def run_roles(
256
+ self,
257
+ role_names: List[str],
258
+ on_progress: Optional[Callable[[str], None]] = None,
259
+ ) -> QESessionResult:
260
+ """
261
+ Run specific QE roles.
262
+
263
+ Args:
264
+ role_names: List of role names (e.g., ["api_tester", "security_tester"])
265
+ on_progress: Optional progress callback
266
+
267
+ Returns:
268
+ Combined QESessionResult from all roles
269
+ """
270
+ from datetime import datetime
271
+ import time
272
+
273
+ started_at = datetime.now()
274
+ start_time = time.monotonic()
275
+
276
+ all_findings = []
277
+ all_errors = []
278
+ total_tests = 0
279
+ tests_passed = 0
280
+ tests_failed = 0
281
+ tests_skipped = 0
282
+ verified_fixes = []
283
+
284
+ # Run each role
285
+ total_roles = len(role_names)
286
+ for idx, role_name in enumerate(role_names, 1):
287
+ try:
288
+ role_start_time = time.monotonic()
289
+ if self.output_format == "rich":
290
+ console.print(f"[cyan]Running role: {role_name} ({idx}/{total_roles})[/cyan]")
291
+ elif self.verbose:
292
+ print(f"Running role: {role_name} ({idx}/{total_roles})")
293
+
294
+ role = get_role(
295
+ role_name,
296
+ self.project_root,
297
+ allow_suggestions=self.allow_suggestions,
298
+ )
299
+ result = await role.run()
300
+
301
+ role_duration = time.monotonic() - role_start_time
302
+ if self.output_format == "rich":
303
+ console.print(f"[dim]โœ“ {role_name} completed in {role_duration:.1f}s[/dim]")
304
+ elif self.verbose:
305
+ print(f"โœ“ {role_name} completed in {role_duration:.1f}s")
306
+
307
+ # Aggregate results
308
+ if result.role_type == RoleType.EXECUTION:
309
+ total_tests += result.tests_run
310
+ tests_passed += result.tests_passed
311
+ tests_failed += result.tests_failed
312
+ tests_skipped += result.tests_skipped
313
+
314
+ # Convert findings to common format
315
+ for finding in result.findings:
316
+ all_findings.append(finding)
317
+
318
+ all_errors.extend(result.errors)
319
+
320
+ except Exception as e:
321
+ all_errors.append(f"Role {role_name} failed: {e}")
322
+ logger.exception(f"Role {role_name} failed")
323
+
324
+ # Apply noise filter to findings
325
+ filtered_findings = self._apply_noise_filter(all_findings)
326
+
327
+ # Process suggestions if enabled
328
+ if self.allow_suggestions and self._suggestion_mode:
329
+ verified_fixes = await self._process_suggestions(filtered_findings)
330
+
331
+ # Build result
332
+ duration = time.monotonic() - start_time
333
+ ended_at = datetime.now()
334
+
335
+ # Create a synthetic result
336
+ from .session import QESessionResult, QEStatus
337
+
338
+ result = QESessionResult(
339
+ session_id=f"roles-{started_at.strftime('%Y%m%d-%H%M%S')}",
340
+ mode=QEMode.QUICK_SCAN, # Default
341
+ status=QEStatus.COMPLETED if not all_errors else QEStatus.FAILED,
342
+ started_at=started_at,
343
+ ended_at=ended_at,
344
+ duration_seconds=duration,
345
+ findings=filtered_findings,
346
+ total_tests=total_tests,
347
+ tests_passed=tests_passed,
348
+ tests_failed=tests_failed,
349
+ tests_skipped=tests_skipped,
350
+ errors=all_errors,
351
+ verified_fixes=verified_fixes,
352
+ allow_suggestions_enabled=self.allow_suggestions,
353
+ )
354
+
355
+ if self.output_format == "rich":
356
+ self._display_result(result)
357
+
358
+ return result
359
+
360
+ async def _process_suggestions(
361
+ self,
362
+ findings: List[Dict[str, Any]],
363
+ ) -> List[Dict[str, Any]]:
364
+ """
365
+ Process suggestions for findings with suggested_fix.
366
+
367
+ When allow_suggestions is enabled, this:
368
+ 1. Finds all findings with suggested fixes
369
+ 2. Verifies each fix in sandbox
370
+ 3. Collects before/after evidence
371
+ 4. Returns verification results
372
+
373
+ All changes are automatically reverted by the workspace manager.
374
+ """
375
+ if not self._suggestion_mode:
376
+ return []
377
+
378
+ verified = []
379
+
380
+ for finding in findings:
381
+ if not finding.get("suggested_fix"):
382
+ continue
383
+
384
+ if self.verbose and self.output_format == "rich":
385
+ console.print(
386
+ f"[yellow]Verifying fix for: {finding.get('title', 'unknown')}[/yellow]"
387
+ )
388
+
389
+ result = self._suggestion_mode.verify_finding_fix(finding)
390
+ if result:
391
+ verified.append(
392
+ {
393
+ "finding_id": finding.get("id"),
394
+ "finding_title": finding.get("title"),
395
+ "status": result.status.value,
396
+ "is_improvement": result.is_improvement,
397
+ "confidence": result.confidence_score,
398
+ "tests_fixed": result.tests_fixed,
399
+ "tests_broken": result.tests_broken,
400
+ "evidence": result.evidence,
401
+ }
402
+ )
403
+
404
+ # Log summary
405
+ if self.verbose and self.output_format == "rich" and verified:
406
+ summary = self._suggestion_mode.get_summary()
407
+ console.print(
408
+ f"[green]Suggestion verification complete: "
409
+ f"{summary['verified']}/{summary['total']} verified, "
410
+ f"{summary['improvements']} improvements[/green]"
411
+ )
412
+
413
+ return verified
414
+
415
+ def _apply_noise_filter(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
416
+ """Apply noise filter to findings."""
417
+ if not findings:
418
+ return []
419
+
420
+ # Convert dict findings to Finding objects
421
+ noise_findings = []
422
+ for f in findings:
423
+ nf = NoiseFinding(
424
+ id=f.get("id", ""),
425
+ severity=f.get("severity", "info"),
426
+ title=f.get("title", ""),
427
+ description=f.get("description", ""),
428
+ file_path=f.get("file_path"),
429
+ line_number=f.get("line_number"),
430
+ evidence=f.get("evidence"),
431
+ suggested_fix=f.get("suggested_fix"),
432
+ confidence=f.get("confidence", 1.0),
433
+ category=f.get("category", ""),
434
+ rule_id=f.get("rule_id"),
435
+ )
436
+ noise_findings.append(nf)
437
+
438
+ # Apply filter
439
+ filtered = self.noise_filter.apply(noise_findings)
440
+
441
+ # Convert back to dicts
442
+ return [f.to_dict() for f in filtered]
443
+
444
+ async def _run_session(
445
+ self,
446
+ config: QESessionConfig,
447
+ on_progress: Optional[Callable[[str], None]],
448
+ ) -> QESessionResult:
449
+ """Internal method to run a session with progress reporting."""
450
+ worktree_info: Optional[WorktreeInfo] = None
451
+ session_root = self.project_root
452
+
453
+ if self.use_worktree:
454
+ try:
455
+ session_id = f"qe-{datetime.now().strftime('%Y%m%d%H%M%S')}"
456
+ worktree_info = await prepare_qe_worktree(self.project_root, session_id)
457
+ session_root = worktree_info.path
458
+
459
+ # Keep artifacts in the original repo by linking .superqode into the worktree.
460
+ original_superqode = self.project_root / ".superqode"
461
+ original_superqode.mkdir(parents=True, exist_ok=True)
462
+ worktree_superqode = session_root / ".superqode"
463
+ if worktree_superqode.exists() or worktree_superqode.is_symlink():
464
+ if worktree_superqode.is_dir() and not worktree_superqode.is_symlink():
465
+ shutil.rmtree(worktree_superqode)
466
+ else:
467
+ worktree_superqode.unlink()
468
+ os.symlink(original_superqode, worktree_superqode, target_is_directory=True)
469
+ except Exception as exc:
470
+ if worktree_info is not None:
471
+ manager = GitWorktreeManager(self.project_root)
472
+ try:
473
+ await manager.remove_worktree(worktree_info, force=True)
474
+ except Exception as cleanup_exc:
475
+ logger.warning(
476
+ "Failed to clean up worktree %s: %s",
477
+ worktree_info.path,
478
+ cleanup_exc,
479
+ )
480
+ logger.warning(
481
+ "Worktree isolation unavailable (%s). Falling back to snapshot isolation.", exc
482
+ )
483
+ session_root = self.project_root
484
+ worktree_info = None
485
+
486
+ session = QESession(session_root, config)
487
+ self._current_session = session
488
+
489
+ try:
490
+ if self.output_format == "rich":
491
+ result = await self._run_with_rich_progress(session)
492
+ else:
493
+ result = await session.run()
494
+ finally:
495
+ if worktree_info is not None:
496
+ manager = GitWorktreeManager(self.project_root)
497
+ try:
498
+ await manager.remove_worktree(worktree_info, force=True)
499
+ except Exception as exc:
500
+ logger.warning("Failed to clean up worktree %s: %s", worktree_info.path, exc)
501
+
502
+ self._run_superopt_hook(result)
503
+ return result
504
+
505
+ def _run_superopt_hook(self, result: QESessionResult) -> None:
506
+ """Run SuperOpt command hook if enabled."""
507
+ from superqode.optimization import load_optimize_config
508
+
509
+ config = load_optimize_config(self.project_root)
510
+ if not config.enabled:
511
+ return
512
+
513
+ artifacts_dir = self.project_root / ".superqode" / "qe-artifacts" / "superopt"
514
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
515
+
516
+ trace_path = artifacts_dir / f"trace-{result.session_id}.json"
517
+ output_path = artifacts_dir / "env.json"
518
+ trace_path.write_text(json.dumps(result.to_dict(), indent=2))
519
+
520
+ if config.command:
521
+ command = config.command
522
+ else:
523
+ command = (
524
+ "python -m superqode.integrations.superopt_runner "
525
+ f"--trace {trace_path} --out {output_path} --project-root {self.project_root}"
526
+ )
527
+
528
+ command = command.format(
529
+ trace_path=trace_path,
530
+ output_path=output_path,
531
+ project_root=self.project_root,
532
+ session_id=result.session_id,
533
+ )
534
+
535
+ try:
536
+ subprocess.run(
537
+ command,
538
+ cwd=self.project_root,
539
+ shell=True,
540
+ check=True,
541
+ timeout=config.timeout_seconds,
542
+ )
543
+ except subprocess.SubprocessError as exc:
544
+ logger.warning("SuperOpt hook failed: %s", exc)
545
+
546
+ async def _run_with_rich_progress(self, session: QESession) -> QESessionResult:
547
+ """Run session with rich console progress output."""
548
+ mode_name = "Quick Scan" if session.config.mode == QEMode.QUICK_SCAN else "Deep QE"
549
+
550
+ with Progress(
551
+ SpinnerColumn(),
552
+ TextColumn("[progress.description]{task.description}"),
553
+ TimeElapsedColumn(),
554
+ console=console,
555
+ ) as progress:
556
+ task = progress.add_task(f"[cyan]SuperQode {mode_name}...", total=None)
557
+
558
+ result = await session.run()
559
+
560
+ progress.update(task, completed=True)
561
+
562
+ # Display results
563
+ self._display_result(result)
564
+
565
+ return result
566
+
567
+ def _display_result(self, result: QESessionResult) -> None:
568
+ """Display session result in rich format."""
569
+ # Header
570
+ mode_emoji = "โšก" if result.mode == QEMode.QUICK_SCAN else "๐Ÿ”ฌ"
571
+ mode_name = "Quick Scan" if result.mode == QEMode.QUICK_SCAN else "Deep QE"
572
+
573
+ console.print()
574
+ console.print(
575
+ Panel(
576
+ Text(
577
+ f"{mode_emoji} SuperQode {mode_name} Complete", justify="center", style="bold"
578
+ ),
579
+ subtitle=f"Session: {result.session_id}",
580
+ )
581
+ )
582
+ console.print()
583
+
584
+ # Verdict
585
+ console.print(f"[bold]Verdict:[/bold] {result.verdict}")
586
+ console.print(f"[dim]Duration: {result.duration_seconds:.1f}s[/dim]")
587
+ console.print()
588
+
589
+ # Test Summary Table
590
+ table = Table(title="Test Results", show_header=True, header_style="bold")
591
+ table.add_column("Suite", style="cyan")
592
+ table.add_column("Total", justify="right")
593
+ table.add_column("Passed", justify="right", style="green")
594
+ table.add_column("Failed", justify="right", style="red")
595
+ table.add_column("Skipped", justify="right", style="yellow")
596
+ table.add_column("Status", justify="center")
597
+
598
+ for name, suite_result in [
599
+ ("Smoke", result.smoke_result),
600
+ ("Sanity", result.sanity_result),
601
+ ("Regression", result.regression_result),
602
+ ]:
603
+ if suite_result:
604
+ status = "โœ…" if suite_result.success else "โŒ"
605
+ table.add_row(
606
+ name,
607
+ str(suite_result.total_tests),
608
+ str(suite_result.passed),
609
+ str(suite_result.failed),
610
+ str(suite_result.skipped),
611
+ status,
612
+ )
613
+ else:
614
+ table.add_row(name, "-", "-", "-", "-", "โญ๏ธ Skipped")
615
+
616
+ # Total row
617
+ table.add_section()
618
+ total_status = "โœ…" if result.tests_failed == 0 else "โŒ"
619
+ table.add_row(
620
+ "[bold]Total[/bold]",
621
+ f"[bold]{result.total_tests}[/bold]",
622
+ f"[bold green]{result.tests_passed}[/bold green]",
623
+ f"[bold red]{result.tests_failed}[/bold red]",
624
+ f"[bold yellow]{result.tests_skipped}[/bold yellow]",
625
+ total_status,
626
+ )
627
+
628
+ console.print(table)
629
+ console.print()
630
+
631
+ # Findings
632
+ if result.findings:
633
+ console.print("[bold]AI Analysis Findings:[/bold]")
634
+ for finding in result.findings:
635
+ title = self._format_finding_text(finding.get("title", "Unknown"), 120)
636
+ description = self._format_finding_text(finding.get("description", ""), 180)
637
+ severity_style = {
638
+ "critical": "bold red",
639
+ "warning": "yellow",
640
+ "info": "blue",
641
+ }.get(finding.get("severity", "info"), "white")
642
+
643
+ console.print(
644
+ f" [{severity_style}]{finding.get('severity', '').upper()}[/{severity_style}]: "
645
+ f"{title}"
646
+ )
647
+ if description and description != title:
648
+ console.print(f" [dim]{description}[/dim]")
649
+ if finding.get("file_path"):
650
+ console.print(
651
+ f" [dim]Location: {finding['file_path']}"
652
+ f"{':' + str(finding['line_number']) if finding.get('line_number') else ''}[/dim]"
653
+ )
654
+
655
+ # Show tool calls if available
656
+ if finding.get("tool_calls"):
657
+ console.print(
658
+ f" [dim]๐Ÿ”ง Tools Used: {', '.join(finding['tool_calls'])}[/dim]"
659
+ )
660
+
661
+ # Show work log summary if available
662
+ if finding.get("work_log") and len(finding["work_log"]) > 0:
663
+ console.print(
664
+ f" [dim]๐Ÿ“‹ Agent performed {len(finding['work_log'])} analysis steps[/dim]"
665
+ )
666
+ console.print()
667
+
668
+ # Artifacts
669
+ if result.patches_generated > 0 or result.tests_generated > 0:
670
+ console.print("[bold]Generated Artifacts:[/bold]")
671
+ if result.patches_generated > 0:
672
+ console.print(f" ๐Ÿ“ Patches: {result.patches_generated}")
673
+ if result.tests_generated > 0:
674
+ console.print(f" ๐Ÿงช Tests: {result.tests_generated}")
675
+ console.print()
676
+
677
+ # QIR location
678
+ if result.qr_path:
679
+ console.print("[bold green]๐Ÿ“‹ Quality Report (QR) generated![/bold green]")
680
+ console.print(f"[green]๐Ÿ“„ View detailed findings: {result.qr_path}[/green]")
681
+ console.print(
682
+ f"[dim]๐Ÿ’ก QR contains evidence-based analysis with {len(result.findings)} findings[/dim]"
683
+ )
684
+ console.print(f"[dim]๐Ÿ” Agent work logs available in the QR for transparency[/dim]")
685
+ elif len(result.findings) > 0:
686
+ console.print("[yellow]โš ๏ธ Findings detected but QR generation failed[/yellow]")
687
+ console.print("[dim]Check .superqode/qe-artifacts/qr/ for reports[/dim]")
688
+
689
+ # Errors
690
+ if result.errors:
691
+ console.print("[bold red]Errors:[/bold red]")
692
+ for error in result.errors:
693
+ console.print(f" โš ๏ธ {error}")
694
+ console.print()
695
+
696
+ def cancel(self) -> None:
697
+ """Cancel the currently running session."""
698
+ if self._current_session:
699
+ self._current_session.cancel()
700
+
701
+ def export_json(self, result: QESessionResult) -> str:
702
+ """Export result as JSON string."""
703
+ return json.dumps(result.to_dict(), indent=2)
704
+
705
+ @staticmethod
706
+ def _format_finding_text(text: str, max_len: int) -> str:
707
+ """Clean and truncate finding text for console output."""
708
+ if not text:
709
+ return ""
710
+ cleaned = text.replace("\\n", " ").replace("\n", " ").replace("\r", " ")
711
+ cleaned = " ".join(cleaned.split())
712
+ if cleaned.lower().startswith("description:"):
713
+ cleaned = cleaned[len("description:") :].strip()
714
+ return (cleaned[: max_len - 1] + "โ€ฆ") if len(cleaned) > max_len else cleaned
715
+
716
+ def export_junit(self, result: QESessionResult) -> str:
717
+ """Export result as JUnit XML for CI integration."""
718
+ lines = [
719
+ '<?xml version="1.0" encoding="UTF-8"?>',
720
+ f'<testsuites name="SuperQode QE" tests="{result.total_tests}" '
721
+ f'failures="{result.tests_failed}" errors="0" '
722
+ f'time="{result.duration_seconds:.3f}">',
723
+ ]
724
+
725
+ for name, suite_result in [
726
+ ("smoke", result.smoke_result),
727
+ ("sanity", result.sanity_result),
728
+ ("regression", result.regression_result),
729
+ ]:
730
+ if suite_result:
731
+ lines.append(
732
+ f' <testsuite name="{name}" tests="{suite_result.total_tests}" '
733
+ f'failures="{suite_result.failed}" errors="{suite_result.errors}" '
734
+ f'skipped="{suite_result.skipped}" time="{suite_result.duration_seconds:.3f}">'
735
+ )
736
+
737
+ for test in suite_result.tests:
738
+ lines.append(
739
+ f' <testcase name="{test.name}" time="{test.duration_seconds:.3f}"'
740
+ )
741
+
742
+ if test.status.value == "failed":
743
+ lines.append(">")
744
+ error_msg = test.error_message or "Test failed"
745
+ lines.append(
746
+ f' <failure message="{error_msg[:100]}">{error_msg}</failure>'
747
+ )
748
+ lines.append(" </testcase>")
749
+ elif test.status.value == "skipped":
750
+ lines.append(">")
751
+ lines.append(" <skipped/>")
752
+ lines.append(" </testcase>")
753
+ else:
754
+ lines.append("/>")
755
+
756
+ lines.append(" </testsuite>")
757
+
758
+ lines.append("</testsuites>")
759
+ return "\n".join(lines)
760
+
761
+
762
+ # Convenience functions for CLI
763
+ async def run_quick_scan(
764
+ project_root: Path,
765
+ verbose: bool = False,
766
+ ) -> QESessionResult:
767
+ """Run a quick scan QE session."""
768
+ orchestrator = QEOrchestrator(project_root, verbose=verbose)
769
+ return await orchestrator.quick_scan()
770
+
771
+
772
+ async def run_deep_qe(
773
+ project_root: Path,
774
+ verbose: bool = False,
775
+ ) -> QESessionResult:
776
+ """Run a deep QE session."""
777
+ orchestrator = QEOrchestrator(project_root, verbose=verbose)
778
+ return await orchestrator.deep_qe()