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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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}")
|