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,713 @@
1
+ """
2
+ QE Session - Orchestrates a complete QE run.
3
+
4
+ A QE session encompasses:
5
+ 1. Workspace setup (ephemeral edit mode)
6
+ 2. Test execution (smoke/sanity/regression)
7
+ 3. Patch validation via harness
8
+ 4. Agent-driven analysis (if enabled)
9
+ 5. Artifact generation (patches, tests, QIR)
10
+ 6. Cleanup (revert all changes, preserve artifacts)
11
+
12
+ Aligned with PRD:
13
+ > "SuperQode never edits, rewrites, or commits code."
14
+ > "All fixes are suggested, validated, and proven, never auto-applied."
15
+ """
16
+
17
+ import asyncio
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from datetime import datetime
21
+ from enum import Enum
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+ import logging
25
+
26
+ from superqode.workspace.manager import WorkspaceManager, QESessionConfig as WorkspaceConfig
27
+ from superqode.workspace.manager import QEMode as WorkspaceQEMode
28
+ from superqode.execution.runner import (
29
+ TestRunner,
30
+ SmokeRunner,
31
+ SanityRunner,
32
+ RegressionRunner,
33
+ TestSuiteResult,
34
+ )
35
+ from superqode.execution.modes import (
36
+ QEMode,
37
+ QuickScanConfig,
38
+ DeepQEConfig,
39
+ get_qe_mode_config,
40
+ )
41
+ from superqode.harness import PatchHarness, HarnessResult
42
+ from superqode.guidance import QEGuidance, GuidanceMode, load_guidance_config
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class QEStatus(Enum):
48
+ """Status of a QE session."""
49
+
50
+ PENDING = "pending"
51
+ RUNNING = "running"
52
+ COMPLETED = "completed"
53
+ FAILED = "failed"
54
+ CANCELLED = "cancelled"
55
+
56
+
57
+ @dataclass
58
+ class QESessionConfig:
59
+ """Configuration for a QE session."""
60
+
61
+ mode: QEMode = QEMode.QUICK_SCAN
62
+
63
+ # Test patterns (multi-language support - includes standard naming)
64
+ smoke_pattern: str = "**/*smoke*"
65
+ sanity_pattern: str = "**/*sanity*"
66
+ regression_pattern: str = "**/*test* **/*spec* **/test_*"
67
+
68
+ # Which test types to run
69
+ run_smoke: bool = True
70
+ run_sanity: bool = True
71
+ run_regression: bool = True
72
+
73
+ # Agent-driven analysis
74
+ run_agent_analysis: bool = True
75
+ agent_roles: List[str] = field(default_factory=list)
76
+
77
+ # Generation options
78
+ generate_tests: bool = False
79
+ generate_patches: bool = False
80
+
81
+ # Execution limits
82
+ timeout_seconds: int = 300
83
+ fail_fast: bool = False
84
+ verbose: bool = False
85
+
86
+ # QIR options
87
+ generate_qir: bool = True
88
+
89
+ @classmethod
90
+ def quick_scan(cls) -> "QESessionConfig":
91
+ """Create a quick scan configuration."""
92
+ config = get_qe_mode_config(QEMode.QUICK_SCAN)
93
+ return cls(
94
+ mode=QEMode.QUICK_SCAN,
95
+ run_smoke=config.run_smoke,
96
+ run_sanity=config.run_sanity,
97
+ run_regression=config.run_regression,
98
+ run_agent_analysis=False, # Quick scan skips deep analysis
99
+ generate_tests=config.generate_tests,
100
+ generate_patches=config.generate_patches,
101
+ timeout_seconds=config.timeout_seconds,
102
+ fail_fast=config.fail_fast,
103
+ )
104
+
105
+ @classmethod
106
+ def deep_qe(cls) -> "QESessionConfig":
107
+ """Create a deep QE configuration."""
108
+ config = get_qe_mode_config(QEMode.DEEP_QE)
109
+ return cls(
110
+ mode=QEMode.DEEP_QE,
111
+ run_smoke=config.run_smoke,
112
+ run_sanity=config.run_sanity,
113
+ run_regression=config.run_regression,
114
+ run_agent_analysis=True,
115
+ generate_tests=config.generate_tests,
116
+ generate_patches=config.generate_patches,
117
+ timeout_seconds=config.timeout_seconds,
118
+ fail_fast=config.fail_fast,
119
+ )
120
+
121
+
122
+ @dataclass
123
+ class QESessionResult:
124
+ """Result of a QE session."""
125
+
126
+ session_id: str
127
+ mode: QEMode
128
+ status: QEStatus
129
+
130
+ started_at: datetime
131
+ ended_at: Optional[datetime] = None
132
+ duration_seconds: float = 0.0
133
+
134
+ # Test results
135
+ smoke_result: Optional[TestSuiteResult] = None
136
+ sanity_result: Optional[TestSuiteResult] = None
137
+ regression_result: Optional[TestSuiteResult] = None
138
+
139
+ # Agent analysis results
140
+ findings: List[Dict[str, Any]] = field(default_factory=list)
141
+
142
+ # Verified fixes (when suggestions are enabled)
143
+ verified_fixes: List[Dict[str, Any]] = field(default_factory=list)
144
+ allow_suggestions_enabled: bool = False
145
+
146
+ # Artifacts
147
+ patches_generated: int = 0
148
+ tests_generated: int = 0
149
+ qr_path: Optional[str] = None
150
+
151
+ # Summary
152
+ total_tests: int = 0
153
+ tests_passed: int = 0
154
+ tests_failed: int = 0
155
+ tests_skipped: int = 0
156
+
157
+ errors: List[str] = field(default_factory=list)
158
+
159
+ @property
160
+ def success(self) -> bool:
161
+ """Did the QE session pass?"""
162
+ # Success requires completed status, no failed tests, AND at least some tests exist
163
+ return self.status == QEStatus.COMPLETED and self.tests_failed == 0 and self.total_tests > 0
164
+
165
+ @property
166
+ def verdict(self) -> str:
167
+ """Human-readable verdict."""
168
+ if self.status == QEStatus.FAILED:
169
+ return "🔴 FAILED - Session error"
170
+ elif self.status == QEStatus.CANCELLED:
171
+ return "⚪ CANCELLED"
172
+ elif self.tests_failed > 0:
173
+ return f"🔴 FAIL - {self.tests_failed} tests failed"
174
+ elif self.total_tests == 0:
175
+ return "🟠 NO TESTS DETECTED - Add tests for proper validation"
176
+ elif len([f for f in self.findings if f.get("severity") == "critical"]) > 0:
177
+ return "🔴 FAIL - Critical issues found"
178
+ elif len([f for f in self.findings if f.get("severity") == "warning"]) > 0:
179
+ return "🟡 CONDITIONAL PASS - Warnings found"
180
+ else:
181
+ return "🟢 PASS"
182
+
183
+ def to_dict(self) -> Dict[str, Any]:
184
+ return {
185
+ "session_id": self.session_id,
186
+ "mode": self.mode.value,
187
+ "status": self.status.value,
188
+ "started_at": self.started_at.isoformat(),
189
+ "ended_at": self.ended_at.isoformat() if self.ended_at else None,
190
+ "duration_seconds": self.duration_seconds,
191
+ "verdict": self.verdict,
192
+ "smoke_result": self.smoke_result.to_dict() if self.smoke_result else None,
193
+ "sanity_result": self.sanity_result.to_dict() if self.sanity_result else None,
194
+ "regression_result": self.regression_result.to_dict()
195
+ if self.regression_result
196
+ else None,
197
+ "findings": self.findings,
198
+ "patches_generated": self.patches_generated,
199
+ "tests_generated": self.tests_generated,
200
+ "qr_path": self.qr_path,
201
+ "total_tests": self.total_tests,
202
+ "tests_passed": self.tests_passed,
203
+ "tests_failed": self.tests_failed,
204
+ "tests_skipped": self.tests_skipped,
205
+ "errors": self.errors,
206
+ }
207
+
208
+
209
+ class QESession:
210
+ """
211
+ Orchestrates a complete QE session.
212
+
213
+ A session:
214
+ 1. Sets up ephemeral workspace
215
+ 2. Runs configured test suites
216
+ 3. Validates patches via harness
217
+ 4. Optionally runs agent-driven analysis (with guidance prompts)
218
+ 5. Generates artifacts
219
+ 6. Cleans up (reverts changes, preserves artifacts)
220
+
221
+ PRD alignment:
222
+ - Never modifies production code
223
+ - Produces QIRs (Quality Investigation Reports)
224
+ - All fixes are suggested and validated, never auto-applied
225
+ """
226
+
227
+ def __init__(
228
+ self,
229
+ project_root: Path,
230
+ config: Optional[QESessionConfig] = None,
231
+ ):
232
+ self.project_root = project_root.resolve()
233
+ self.config = config or QESessionConfig()
234
+
235
+ self.workspace = WorkspaceManager(self.project_root)
236
+ self._session_id: Optional[str] = None
237
+ self._result: Optional[QESessionResult] = None
238
+ self._cancelled = False
239
+
240
+ # Initialize harness and guidance
241
+ self.harness = PatchHarness(self.project_root)
242
+ guidance_config = load_guidance_config(self.project_root)
243
+ guidance_mode = (
244
+ GuidanceMode.QUICK_SCAN
245
+ if self.config.mode == QEMode.QUICK_SCAN
246
+ else GuidanceMode.DEEP_QE
247
+ )
248
+ self.guidance = QEGuidance(config=guidance_config, mode=guidance_mode)
249
+
250
+ @property
251
+ def session_id(self) -> Optional[str]:
252
+ return self._session_id
253
+
254
+ @property
255
+ def result(self) -> Optional[QESessionResult]:
256
+ return self._result
257
+
258
+ async def run(self) -> QESessionResult:
259
+ """
260
+ Run the complete QE session.
261
+
262
+ Returns QESessionResult with all findings and artifacts.
263
+ """
264
+ started_at = datetime.now()
265
+ start_time = time.monotonic()
266
+
267
+ # Initialize result
268
+ self._result = QESessionResult(
269
+ session_id="",
270
+ mode=self.config.mode,
271
+ status=QEStatus.RUNNING,
272
+ started_at=started_at,
273
+ )
274
+
275
+ try:
276
+ # Start workspace session
277
+ workspace_config = WorkspaceConfig(
278
+ mode=WorkspaceQEMode.QUICK_SCAN
279
+ if self.config.mode == QEMode.QUICK_SCAN
280
+ else WorkspaceQEMode.DEEP_QE,
281
+ timeout_seconds=self.config.timeout_seconds,
282
+ generate_tests=self.config.generate_tests,
283
+ generate_patches=self.config.generate_patches,
284
+ )
285
+
286
+ self._session_id = self.workspace.start_session(config=workspace_config)
287
+ self._result.session_id = self._session_id
288
+
289
+ logger.info(f"Started QE session: {self._session_id}")
290
+
291
+ # Run test suites
292
+ await self._run_tests()
293
+
294
+ # Run linting if configured
295
+ await self._run_lint_role()
296
+
297
+ # Check if cancelled
298
+ if self._cancelled:
299
+ self._result.status = QEStatus.CANCELLED
300
+ return self._finalize_result(start_time)
301
+
302
+ # Run agent analysis (if enabled and in deep mode)
303
+ if self.config.run_agent_analysis and self.config.mode == QEMode.DEEP_QE:
304
+ try:
305
+ if self.config.verbose:
306
+ print("🤖 Starting AI agent analysis...")
307
+ await self._run_agent_analysis()
308
+ if self.config.verbose:
309
+ print("✅ AI agent analysis completed")
310
+ except Exception as e:
311
+ if self.config.verbose:
312
+ print(f"❌ Agent analysis failed: {e}")
313
+ logger.error(f"Agent analysis failed: {e}")
314
+ # Continue with session even if agent analysis fails
315
+
316
+ # Mark completed
317
+ self._result.status = QEStatus.COMPLETED
318
+
319
+ except asyncio.TimeoutError:
320
+ self._result.status = QEStatus.FAILED
321
+ self._result.errors.append(f"Session timed out after {self.config.timeout_seconds}s")
322
+ logger.error(f"QE session timed out: {self._session_id}")
323
+
324
+ except Exception as e:
325
+ self._result.status = QEStatus.FAILED
326
+ self._result.errors.append(str(e))
327
+ logger.exception(f"QE session failed: {self._session_id}")
328
+
329
+ finally:
330
+ return self._finalize_result(start_time)
331
+
332
+ async def _run_tests(self) -> None:
333
+ """Run configured test suites."""
334
+ # Smoke tests
335
+ if self.config.run_smoke:
336
+ logger.info("Running smoke tests...")
337
+ runner = SmokeRunner(
338
+ self.project_root,
339
+ test_pattern=self.config.smoke_pattern,
340
+ timeout_seconds=min(60, self.config.timeout_seconds),
341
+ )
342
+ self._result.smoke_result = await runner.run()
343
+ self._update_test_counts(self._result.smoke_result)
344
+
345
+ # Fail fast if smoke tests fail in quick scan mode
346
+ if (
347
+ self.config.fail_fast
348
+ and self._result.smoke_result
349
+ and not self._result.smoke_result.success
350
+ ):
351
+ logger.info("Smoke tests failed, stopping due to fail-fast")
352
+ return
353
+
354
+ # Sanity tests
355
+ if self.config.run_sanity:
356
+ logger.info("Running sanity tests...")
357
+ runner = SanityRunner(
358
+ self.project_root,
359
+ test_pattern=self.config.sanity_pattern,
360
+ timeout_seconds=min(120, self.config.timeout_seconds),
361
+ )
362
+ self._result.sanity_result = await runner.run()
363
+ self._update_test_counts(self._result.sanity_result)
364
+
365
+ if (
366
+ self.config.fail_fast
367
+ and self._result.sanity_result
368
+ and not self._result.sanity_result.success
369
+ ):
370
+ logger.info("Sanity tests failed, stopping due to fail-fast")
371
+ return
372
+
373
+ # Check if any tests were found so far
374
+ total_tests_found = (
375
+ self._result.smoke_result.total_tests if self._result.smoke_result else 0
376
+ ) + (self._result.sanity_result.total_tests if self._result.sanity_result else 0)
377
+
378
+ # Fallback: If no smoke/sanity tests found in quick scan, run regression tests
379
+ # This ensures users get immediate feedback even with non-standard test naming
380
+ should_run_regression = self.config.run_regression
381
+ if (
382
+ not should_run_regression
383
+ and self.config.mode == QEMode.QUICK_SCAN
384
+ and total_tests_found == 0
385
+ ):
386
+ logger.info(
387
+ "No smoke/sanity tests found, falling back to regression tests for immediate feedback"
388
+ )
389
+ should_run_regression = True
390
+
391
+ if should_run_regression:
392
+ logger.info("Running regression tests...")
393
+ mode_config = get_qe_mode_config(self.config.mode)
394
+ detect_flakes = getattr(mode_config, "detect_flakes", False)
395
+ retry_count = getattr(mode_config, "retry_count", 0)
396
+
397
+ runner = RegressionRunner(
398
+ self.project_root,
399
+ test_pattern=self.config.regression_pattern,
400
+ timeout_seconds=self.config.timeout_seconds,
401
+ detect_flakes=detect_flakes,
402
+ retry_count=retry_count,
403
+ )
404
+ self._result.regression_result = await runner.run()
405
+ self._update_test_counts(self._result.regression_result)
406
+
407
+ def _update_test_counts(self, suite_result: TestSuiteResult) -> None:
408
+ """Update total test counts from a suite result."""
409
+ self._result.total_tests += suite_result.total_tests
410
+ self._result.tests_passed += suite_result.passed
411
+ self._result.tests_failed += suite_result.failed
412
+ self._result.tests_skipped += suite_result.skipped
413
+
414
+ async def _run_lint_role(self) -> None:
415
+ """Run the lint tester role if it is configured in YAML."""
416
+ from superqode.superqe.roles import get_role, RoleType
417
+
418
+ try:
419
+ role = get_role("lint_tester", self.project_root, allow_suggestions=False)
420
+ except ValueError:
421
+ return
422
+
423
+ if role.role_type != RoleType.EXECUTION:
424
+ return
425
+
426
+ result = await role.run()
427
+ self._result.findings.extend(result.findings)
428
+ self._result.errors.extend(result.errors)
429
+
430
+ async def _run_agent_analysis(self) -> None:
431
+ """Run agent-driven QE analysis with specialized QE agents."""
432
+ logger.info("Running AI-powered agent analysis...")
433
+
434
+ if not self.config.agent_roles:
435
+ logger.info("No agent roles configured, skipping agent analysis")
436
+ return
437
+
438
+ # Early validation - check if OpenCode is available
439
+ import shutil
440
+
441
+ if not shutil.which("opencode"):
442
+ logger.warning("OpenCode not found - agent analysis will use fallback mode")
443
+ # Continue with fallback findings instead of failing
444
+
445
+ for role_name in self.config.agent_roles:
446
+ if self._cancelled:
447
+ logger.info("Analysis cancelled by user")
448
+ break
449
+
450
+ try:
451
+ # Map QE role name to OpenCode agent name
452
+ from .acp_runner import get_opencode_agent_for_role
453
+
454
+ agent_name = get_opencode_agent_for_role(role_name)
455
+ logger.info(f"Running QE role '{role_name}' using agent '{agent_name}'")
456
+
457
+ # Run real ACP agent analysis using OpenCode
458
+ try:
459
+ from .acp_runner import ACPQERunner, ACPRunnerConfig, get_qe_prompt
460
+
461
+ # Create ACP runner configuration
462
+ acp_config = ACPRunnerConfig(
463
+ agent_command="opencode run --format json", # Use consistent command
464
+ model=None, # Will use default from OpenCode
465
+ timeout_seconds=min(180, self.config.timeout_seconds), # Shorter for QE
466
+ verbose=self.config.verbose,
467
+ allow_suggestions=self.config.generate_patches,
468
+ )
469
+
470
+ # Create and run ACP agent
471
+ runner = ACPQERunner(self.project_root, acp_config)
472
+
473
+ # Get the appropriate QE prompt for this role
474
+ prompt = get_qe_prompt(role_name, acp_config.allow_suggestions)
475
+
476
+ # Run the actual AI analysis
477
+ result = await runner.run(prompt, role_name)
478
+
479
+ # Convert ACP findings to session format
480
+ sample_findings = []
481
+ for acp_finding in result.findings:
482
+ finding_dict = {
483
+ "id": acp_finding.id,
484
+ "severity": acp_finding.severity,
485
+ "title": acp_finding.title,
486
+ "description": acp_finding.description,
487
+ "file_path": acp_finding.file_path,
488
+ "line_number": acp_finding.line_number,
489
+ "evidence": acp_finding.evidence,
490
+ "suggested_fix": acp_finding.suggested_fix,
491
+ "confidence": acp_finding.confidence,
492
+ "category": acp_finding.category,
493
+ "agent": role_name,
494
+ "work_log": self._extract_work_log_from_acp_output(
495
+ result.agent_output, result.tool_calls
496
+ ),
497
+ "tool_calls": [
498
+ tc.get("title", tc.get("id", "unknown")) for tc in result.tool_calls
499
+ ],
500
+ }
501
+ sample_findings.append(finding_dict)
502
+
503
+ # If no findings extracted, create a summary finding
504
+ if not sample_findings:
505
+ sample_findings = [
506
+ {
507
+ "id": f"{role_name}-summary",
508
+ "severity": "info",
509
+ "title": f"🤖 AI {role_name.replace('-', ' ').title()} Analysis Complete",
510
+ "description": f"OpenCode AI agent completed {role_name} analysis. The agent processed the codebase and provided analysis insights.",
511
+ "file_path": None,
512
+ "line_number": None,
513
+ "evidence": f"Agent output: {result.agent_output[:200]}..."
514
+ if result.agent_output
515
+ else "Analysis completed without specific findings",
516
+ "suggested_fix": None,
517
+ "confidence": 0.8,
518
+ "category": role_name,
519
+ "agent": role_name,
520
+ "work_log": self._extract_work_log_from_acp_output(
521
+ result.agent_output, result.tool_calls
522
+ ),
523
+ "tool_calls": [
524
+ tc.get("title", tc.get("id", "unknown"))
525
+ for tc in result.tool_calls
526
+ ],
527
+ }
528
+ ]
529
+
530
+ if result.errors:
531
+ logger.warning(f"ACP agent errors: {result.errors}")
532
+
533
+ except Exception as e:
534
+ logger.error(f"ACP agent analysis failed: {e}")
535
+ # Fallback to basic analysis if ACP fails
536
+ sample_findings = [
537
+ {
538
+ "id": f"{role_name}-fallback",
539
+ "severity": "info",
540
+ "title": f"🤖 {role_name.replace('-', ' ').title()} Analysis (ACP Unavailable)",
541
+ "description": f"AI-powered {role_name} analysis is available but requires OpenCode to be installed and configured.",
542
+ "file_path": None,
543
+ "line_number": None,
544
+ "evidence": f"Install OpenCode to enable real AI analysis: npm i -g opencode-ai",
545
+ "suggested_fix": None,
546
+ "confidence": 0.5,
547
+ "category": role_name,
548
+ "agent": role_name,
549
+ "work_log": ["ACP agent connection attempted but failed"],
550
+ "tool_calls": [],
551
+ }
552
+ ]
553
+
554
+ # Add sample findings to results
555
+ self._result.findings.extend(sample_findings)
556
+
557
+ # Also add findings to workspace for QIR generation
558
+ # Add findings to workspace for QIR generation
559
+ for finding in sample_findings:
560
+ self.workspace.add_finding(
561
+ severity=finding["severity"],
562
+ title=finding["title"],
563
+ description=finding["description"],
564
+ file_path=finding.get("file_path"),
565
+ line_number=finding.get("line_number"),
566
+ evidence=finding.get("evidence", ""),
567
+ suggested_fix=finding.get("suggested_fix", ""),
568
+ work_log=finding.get("work_log"),
569
+ tool_calls=finding.get("tool_calls"),
570
+ )
571
+
572
+ logger.info(
573
+ f"QE agent {role_name} completed: {len(sample_findings)} AI-powered findings generated"
574
+ )
575
+
576
+ except Exception as e:
577
+ logger.error(f"QE agent {role_name} failed: {e}")
578
+ # Continue with other agents even if one fails
579
+ continue
580
+
581
+ logger.info(f"Agent analysis complete: {len(self._result.findings)} total findings")
582
+
583
+ def _extract_work_log_from_acp_output(
584
+ self, agent_output: str, tool_calls: List[Dict[str, Any]]
585
+ ) -> List[str]:
586
+ """Extract work log from ACP agent output and tool calls."""
587
+ work_log = []
588
+
589
+ # Add initialization
590
+ work_log.append("🤖 ACP Agent initialized and connected to OpenCode")
591
+
592
+ # Add tool calls as work steps
593
+ for tool_call in tool_calls:
594
+ title = tool_call.get("title", tool_call.get("id", "unknown"))
595
+ status = tool_call.get("status", "executed")
596
+ work_log.append(f"🔧 Tool Call: {title} - {status}")
597
+
598
+ # Extract reasoning and analysis steps from agent output
599
+ lines = agent_output.split("\n")
600
+ for line in lines:
601
+ line = line.strip()
602
+ if not line:
603
+ continue
604
+
605
+ # Look for reasoning patterns
606
+ if any(
607
+ keyword in line.lower()
608
+ for keyword in [
609
+ "analyzing",
610
+ "checking",
611
+ "reviewing",
612
+ "examining",
613
+ "scanning",
614
+ "parsing",
615
+ ]
616
+ ):
617
+ work_log.append(f"🧠 Reasoning: {line}")
618
+
619
+ # Look for findings
620
+ elif any(
621
+ keyword in line.lower()
622
+ for keyword in ["found", "detected", "identified", "issue", "problem", "warning"]
623
+ ):
624
+ work_log.append(f"⚠️ Analysis: {line}")
625
+
626
+ # Look for completion
627
+ elif any(
628
+ keyword in line.lower() for keyword in ["complete", "finished", "done", "summary"]
629
+ ):
630
+ work_log.append(f"📊 Analysis: {line}")
631
+ break
632
+
633
+ # Add completion if we have findings
634
+ if work_log:
635
+ work_log.append("✅ ACP Agent analysis completed")
636
+
637
+ return work_log
638
+
639
+ async def validate_patches(
640
+ self,
641
+ changes: Dict[Path, str],
642
+ ) -> HarnessResult:
643
+ """
644
+ Validate patches before including in QIR.
645
+
646
+ This is called automatically for any suggested fixes.
647
+ """
648
+ logger.info(f"Validating {len(changes)} file changes via harness...")
649
+ result = await self.harness.validate_changes(changes)
650
+
651
+ if result.success:
652
+ logger.info(f"Harness validation passed ({result.files_validated} files)")
653
+ else:
654
+ logger.warning(
655
+ f"Harness validation found {result.error_count} errors, "
656
+ f"{result.warning_count} warnings"
657
+ )
658
+
659
+ return result
660
+
661
+ def _finalize_result(self, start_time: float) -> QESessionResult:
662
+ """Finalize the session result and cleanup."""
663
+ ended_at = datetime.now()
664
+ duration = time.monotonic() - start_time
665
+
666
+ self._result.ended_at = ended_at
667
+ self._result.duration_seconds = duration
668
+
669
+ # End workspace session (reverts changes, generates QIR)
670
+ try:
671
+ ws_result = self.workspace.end_session(generate_qir=self.config.generate_qir)
672
+ self._result.patches_generated = ws_result.patches_generated
673
+ self._result.tests_generated = ws_result.tests_generated
674
+ self._result.errors.extend(ws_result.errors)
675
+
676
+ # Set QIR path if generated
677
+ if ws_result.qir_generated:
678
+ qir_artifacts = self.workspace.artifacts.list_qirs()
679
+ if qir_artifacts:
680
+ # Get the most recent QIR
681
+ latest_qir = max(qir_artifacts, key=lambda a: a.created_at)
682
+ self._result.qr_path = str(latest_qir.path)
683
+
684
+ # Merge agent findings with workspace findings
685
+ workspace_findings = [
686
+ {
687
+ "id": f.id,
688
+ "severity": f.severity,
689
+ "title": f.title,
690
+ "description": f.description,
691
+ "file_path": f.file_path,
692
+ "line_number": f.line_number,
693
+ }
694
+ for f in self.workspace.get_findings()
695
+ ]
696
+
697
+ # Combine agent findings (added during analysis) with workspace findings
698
+ self._result.findings.extend(workspace_findings)
699
+ except Exception as e:
700
+ self._result.errors.append(f"Cleanup failed: {e}")
701
+ logger.error(f"Failed to finalize session: {e}")
702
+
703
+ logger.info(
704
+ f"QE session completed: {self._result.session_id} - "
705
+ f"{self._result.verdict} ({duration:.1f}s)"
706
+ )
707
+
708
+ return self._result
709
+
710
+ def cancel(self) -> None:
711
+ """Cancel the running session."""
712
+ self._cancelled = True
713
+ logger.info(f"QE session cancellation requested: {self._session_id}")