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,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()
|