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
superqode/commands/qe.py
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQE (Super Quality Engineering) CLI commands.
|
|
3
|
+
|
|
4
|
+
Main entry point for running QE automation with ephemeral workspace guarantees.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Git worktree-based isolation
|
|
8
|
+
- Session coordination with locking
|
|
9
|
+
- JSONL event streaming for CI
|
|
10
|
+
- Structured QR with priorities
|
|
11
|
+
- Constitution system for guardrails
|
|
12
|
+
- Patch harness for validation
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
from rich.markdown import Markdown
|
|
30
|
+
|
|
31
|
+
from superqode.safety import get_safety_warnings, show_safety_warnings, get_warning_acknowledgment
|
|
32
|
+
from superqode.enterprise import require_enterprise
|
|
33
|
+
|
|
34
|
+
console = Console()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _enterprise_only(feature_name: str) -> bool:
|
|
38
|
+
return require_enterprise(feature_name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
def qe():
|
|
43
|
+
"""Quality Engineering automation commands.
|
|
44
|
+
|
|
45
|
+
Run QE sessions with full ephemeral workspace guarantee:
|
|
46
|
+
- Agents can freely modify code for testing
|
|
47
|
+
- All changes are automatically reverted
|
|
48
|
+
- Artifacts (patches, tests, QRs) are preserved
|
|
49
|
+
- Git operations are blocked
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@qe.command("run")
|
|
55
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
56
|
+
@click.option(
|
|
57
|
+
"--mode",
|
|
58
|
+
"-m",
|
|
59
|
+
type=click.Choice(["quick", "deep"]),
|
|
60
|
+
default="quick",
|
|
61
|
+
help="QE mode: quick (fast scan) or deep (full investigation)",
|
|
62
|
+
)
|
|
63
|
+
@click.option("--role", "-r", multiple=True, help="QE role(s) to run (e.g., qe.security_tester)")
|
|
64
|
+
@click.option("--timeout", "-t", type=int, default=None, help="Timeout in seconds")
|
|
65
|
+
@click.option("--no-revert", is_flag=True, help="Don't revert changes (for debugging)")
|
|
66
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory for artifacts")
|
|
67
|
+
@click.option("--json", "json_output", is_flag=True, help="Output results as JSON")
|
|
68
|
+
@click.option("--jsonl", "jsonl_stream", is_flag=True, help="Stream events as JSONL (for CI)")
|
|
69
|
+
@click.option("--junit", type=click.Path(), help="Export JUnit XML to file for CI")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--worktree",
|
|
72
|
+
"use_worktree",
|
|
73
|
+
is_flag=True,
|
|
74
|
+
help="Use git worktree isolation (writes .git/worktrees; opt-in only)",
|
|
75
|
+
)
|
|
76
|
+
@click.option("--generate", "-g", is_flag=True, help="Generate tests for detected issues")
|
|
77
|
+
@click.option(
|
|
78
|
+
"--allow-suggestions",
|
|
79
|
+
is_flag=True,
|
|
80
|
+
help="Enable suggestion mode: agents can fix bugs, verify fixes, then revert. "
|
|
81
|
+
"Patches preserved for user approval.",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--verbose", "-v", is_flag=True, help="Show detailed progress and agent work logs in real-time"
|
|
85
|
+
)
|
|
86
|
+
def qe_run(
|
|
87
|
+
path: str,
|
|
88
|
+
mode: str,
|
|
89
|
+
role: tuple,
|
|
90
|
+
timeout: int,
|
|
91
|
+
no_revert: bool,
|
|
92
|
+
output: str,
|
|
93
|
+
json_output: bool,
|
|
94
|
+
jsonl_stream: bool,
|
|
95
|
+
junit: str,
|
|
96
|
+
use_worktree: bool,
|
|
97
|
+
generate: bool,
|
|
98
|
+
allow_suggestions: bool,
|
|
99
|
+
verbose: bool,
|
|
100
|
+
):
|
|
101
|
+
"""Run a QE session on the specified path.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
|
|
105
|
+
superqe run . # Quick scan current directory
|
|
106
|
+
|
|
107
|
+
superqe run ./src --mode deep # Deep QE on src/ (verbose by default)
|
|
108
|
+
|
|
109
|
+
superqe run . --verbose # Show detailed agent work logs
|
|
110
|
+
|
|
111
|
+
superqe run . -r security_tester -r api_tester
|
|
112
|
+
|
|
113
|
+
superqe run . --junit results.xml # Export for CI
|
|
114
|
+
|
|
115
|
+
superqe run . --jsonl # Stream JSONL events for CI
|
|
116
|
+
|
|
117
|
+
superqe run . --worktree # Use git worktree isolation
|
|
118
|
+
|
|
119
|
+
superqe run . --generate # Generate tests for issues
|
|
120
|
+
|
|
121
|
+
superqe run . --allow-suggestions # Let agents suggest and verify fixes
|
|
122
|
+
"""
|
|
123
|
+
if jsonl_stream or junit or generate or allow_suggestions:
|
|
124
|
+
if not _enterprise_only("Advanced QE automation"):
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
from superqode.superqe import QEOrchestrator, QEEventEmitter, set_event_emitter
|
|
128
|
+
from superqode.workspace import QECoordinator
|
|
129
|
+
from superqode.utils.error_handling import check_dependencies, validate_project_structure
|
|
130
|
+
from superqode.config.loader import find_config_file
|
|
131
|
+
|
|
132
|
+
project_root = Path(path).resolve()
|
|
133
|
+
|
|
134
|
+
if not find_config_file() and not (project_root / "superqode.yaml").exists():
|
|
135
|
+
console.print(
|
|
136
|
+
"[yellow]⚠️ No superqode.yaml found. Run `superqe init` to create one.[/yellow]"
|
|
137
|
+
)
|
|
138
|
+
return 1
|
|
139
|
+
|
|
140
|
+
# Check for basic dependencies first
|
|
141
|
+
if not check_dependencies():
|
|
142
|
+
console.print("[red]❌ Dependency check failed. Please fix issues above.[/red]")
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
# Validate project structure
|
|
146
|
+
issues = validate_project_structure(project_root)
|
|
147
|
+
if issues["errors"]:
|
|
148
|
+
console.print("[red]❌ Project validation errors:[/red]")
|
|
149
|
+
for error in issues["errors"]:
|
|
150
|
+
console.print(f" • {error}")
|
|
151
|
+
console.print("[yellow]💡 Fix these issues and try again.[/yellow]")
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
if issues["warnings"] and not jsonl_stream:
|
|
155
|
+
console.print("[yellow]⚠️ Project warnings:[/yellow]")
|
|
156
|
+
for warning in issues["warnings"]:
|
|
157
|
+
console.print(f" • {warning}")
|
|
158
|
+
|
|
159
|
+
# Setup JSONL streaming if requested
|
|
160
|
+
if jsonl_stream:
|
|
161
|
+
emitter = QEEventEmitter(output=sys.stdout, enabled=True)
|
|
162
|
+
set_event_emitter(emitter)
|
|
163
|
+
# Suppress rich console output when streaming JSONL
|
|
164
|
+
console_output = Console(quiet=True)
|
|
165
|
+
else:
|
|
166
|
+
console_output = console
|
|
167
|
+
|
|
168
|
+
# Show safety warnings for QE sessions
|
|
169
|
+
if not jsonl_stream:
|
|
170
|
+
safety_warnings = get_safety_warnings()
|
|
171
|
+
show_safety_warnings(safety_warnings, console=console_output)
|
|
172
|
+
|
|
173
|
+
# Get acknowledgment for critical warnings
|
|
174
|
+
if not get_warning_acknowledgment(safety_warnings, console=console_output):
|
|
175
|
+
console_output.print("[yellow]Operation cancelled by user.[/yellow]")
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
# Check for conflicting sessions using coordinator
|
|
179
|
+
coordinator = QECoordinator(project_root)
|
|
180
|
+
with coordinator.session(
|
|
181
|
+
f"qe-cli-{datetime.now().strftime('%Y%m%d%H%M%S')}", mode, "CLI QE session"
|
|
182
|
+
) as lock:
|
|
183
|
+
if lock is None:
|
|
184
|
+
if jsonl_stream:
|
|
185
|
+
print(
|
|
186
|
+
json.dumps(
|
|
187
|
+
{
|
|
188
|
+
"type": "qe.blocked",
|
|
189
|
+
"reason": "Another QE session is already running",
|
|
190
|
+
"timestamp": datetime.now().isoformat(),
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
console_output.print("[yellow]Another QE session is already running[/yellow]")
|
|
196
|
+
console_output.print("[dim]Use 'superqe status' to check session status[/dim]")
|
|
197
|
+
return 1
|
|
198
|
+
|
|
199
|
+
# Warn about worktree isolation touching git metadata
|
|
200
|
+
if use_worktree and not jsonl_stream:
|
|
201
|
+
console_output.print(
|
|
202
|
+
"[yellow]Worktree isolation writes to .git/worktrees. "
|
|
203
|
+
"Use only if you are comfortable with git metadata changes.[/yellow]"
|
|
204
|
+
)
|
|
205
|
+
console_output.print()
|
|
206
|
+
|
|
207
|
+
# Show allow_suggestions mode notice
|
|
208
|
+
if allow_suggestions and not jsonl_stream:
|
|
209
|
+
console_output.print()
|
|
210
|
+
console_output.print("[yellow]SUGGESTION MODE ENABLED[/yellow]")
|
|
211
|
+
console_output.print(
|
|
212
|
+
"Agents will fix bugs in an isolated workspace, verify fixes, then revert."
|
|
213
|
+
)
|
|
214
|
+
console_output.print("Patches preserved in .superqode/qe-artifacts/patches/")
|
|
215
|
+
console_output.print()
|
|
216
|
+
|
|
217
|
+
# Verbose output only when explicitly requested
|
|
218
|
+
enable_verbose = verbose
|
|
219
|
+
|
|
220
|
+
# Create orchestrator
|
|
221
|
+
orchestrator = QEOrchestrator(
|
|
222
|
+
project_root,
|
|
223
|
+
verbose=enable_verbose,
|
|
224
|
+
output_format="jsonl" if jsonl_stream else ("json" if json_output else "rich"),
|
|
225
|
+
use_worktree=use_worktree,
|
|
226
|
+
allow_suggestions=allow_suggestions,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Run the appropriate mode or roles
|
|
230
|
+
try:
|
|
231
|
+
# If specific roles are requested, run them
|
|
232
|
+
if role:
|
|
233
|
+
role_list = list(role)
|
|
234
|
+
if not jsonl_stream:
|
|
235
|
+
console_output.print(f"[cyan]Running QE roles: {', '.join(role_list)}[/cyan]")
|
|
236
|
+
console_output.print()
|
|
237
|
+
|
|
238
|
+
result = _run_async(orchestrator.run_roles(role_list))
|
|
239
|
+
elif mode == "quick":
|
|
240
|
+
result = _run_async(orchestrator.quick_scan())
|
|
241
|
+
else:
|
|
242
|
+
result = _run_async(orchestrator.deep_qe())
|
|
243
|
+
|
|
244
|
+
# Handle output options
|
|
245
|
+
if json_output and not jsonl_stream:
|
|
246
|
+
console_output.print(orchestrator.export_json(result))
|
|
247
|
+
|
|
248
|
+
if junit:
|
|
249
|
+
junit_path = Path(junit)
|
|
250
|
+
junit_content = orchestrator.export_junit(result)
|
|
251
|
+
junit_path.write_text(junit_content)
|
|
252
|
+
if not jsonl_stream:
|
|
253
|
+
console_output.print(f"[green]✓[/green] JUnit report saved to {junit_path}")
|
|
254
|
+
|
|
255
|
+
# Return exit code based on result
|
|
256
|
+
return 0 if result.success else 1
|
|
257
|
+
|
|
258
|
+
except KeyboardInterrupt:
|
|
259
|
+
if not jsonl_stream:
|
|
260
|
+
console_output.print("\n[yellow]Session cancelled by user[/yellow]")
|
|
261
|
+
orchestrator.cancel()
|
|
262
|
+
return 130
|
|
263
|
+
except Exception as e:
|
|
264
|
+
if jsonl_stream:
|
|
265
|
+
print(
|
|
266
|
+
json.dumps(
|
|
267
|
+
{
|
|
268
|
+
"type": "qe.error",
|
|
269
|
+
"error": str(e),
|
|
270
|
+
"timestamp": datetime.now().isoformat(),
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
console_output.print(f"[red]Error:[/red] {e}")
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@qe.command("roles")
|
|
280
|
+
def qe_roles():
|
|
281
|
+
"""List all available QE roles."""
|
|
282
|
+
from superqode.superqe import list_roles
|
|
283
|
+
|
|
284
|
+
console.print()
|
|
285
|
+
console.print(Panel("[bold]Available QE Roles[/bold]", border_style="cyan"))
|
|
286
|
+
console.print()
|
|
287
|
+
|
|
288
|
+
roles = list_roles()
|
|
289
|
+
|
|
290
|
+
# Group by type
|
|
291
|
+
execution_roles = [r for r in roles if r["type"] == "execution"]
|
|
292
|
+
detection_roles = [r for r in roles if r["type"] == "detection"]
|
|
293
|
+
heuristic_roles = [r for r in roles if r["type"] == "heuristic"]
|
|
294
|
+
|
|
295
|
+
console.print("[bold]Execution Roles[/bold] (run existing tests)")
|
|
296
|
+
for role in execution_roles:
|
|
297
|
+
console.print(f" [cyan]{role['name']}[/cyan]: {role['description']}")
|
|
298
|
+
console.print()
|
|
299
|
+
|
|
300
|
+
console.print("[bold]Detection Roles[/bold] (AI-powered issue detection)")
|
|
301
|
+
for role in detection_roles:
|
|
302
|
+
console.print(f" [magenta]{role['name']}[/magenta]: {role['description']}")
|
|
303
|
+
if role.get("focus_areas"):
|
|
304
|
+
console.print(f" [dim]Focus: {', '.join(role['focus_areas'])}[/dim]")
|
|
305
|
+
console.print()
|
|
306
|
+
|
|
307
|
+
console.print("[bold]Heuristic Roles[/bold] (senior QE review)")
|
|
308
|
+
for role in heuristic_roles:
|
|
309
|
+
console.print(f" [green]{role['name']}[/green]: {role['description']}")
|
|
310
|
+
console.print()
|
|
311
|
+
|
|
312
|
+
console.print("[dim]Usage: superqe run . -r <role_name> -r <role_name>[/dim]")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@qe.command("behaviors")
|
|
316
|
+
def qe_behaviors():
|
|
317
|
+
"""List available basic QE behaviors."""
|
|
318
|
+
console.print()
|
|
319
|
+
console.print(Panel("[bold]Basic QE Behaviors[/bold]", border_style="cyan"))
|
|
320
|
+
console.print()
|
|
321
|
+
|
|
322
|
+
# Basic behaviors (always available)
|
|
323
|
+
basic_behaviors = {
|
|
324
|
+
"syntax-errors": "Basic syntax validation and linting",
|
|
325
|
+
"code-style": "PEP8 and style checking",
|
|
326
|
+
"imports": "Import organization and dependencies",
|
|
327
|
+
"documentation": "Documentation completeness",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for name, desc in basic_behaviors.items():
|
|
331
|
+
console.print(f" [green]✓[/green] [cyan]{name}[/cyan]: {desc}")
|
|
332
|
+
|
|
333
|
+
console.print()
|
|
334
|
+
console.print("[yellow]🔬 For advanced CodeOptiX behaviors, use:[/yellow]")
|
|
335
|
+
console.print(" [cyan]superqe advanced behaviors[/cyan]")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@qe.command("quick")
|
|
339
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
340
|
+
def qe_quick(path: str):
|
|
341
|
+
"""Run a quick scan QE session (alias for 'qe run --mode quick').
|
|
342
|
+
|
|
343
|
+
Fast, time-boxed QE for pre-commit and developer feedback.
|
|
344
|
+
"""
|
|
345
|
+
from superqode.superqe import QEOrchestrator
|
|
346
|
+
|
|
347
|
+
project_root = Path(path).resolve()
|
|
348
|
+
orchestrator = QEOrchestrator(project_root, verbose=False)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
result = asyncio.get_event_loop().run_until_complete(orchestrator.quick_scan())
|
|
352
|
+
return 0 if result.success else 1
|
|
353
|
+
except KeyboardInterrupt:
|
|
354
|
+
console.print("\n[yellow]Session cancelled[/yellow]")
|
|
355
|
+
return 130
|
|
356
|
+
except Exception as e:
|
|
357
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
358
|
+
return 1
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@qe.command("deep")
|
|
362
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
363
|
+
def qe_deep(path: str):
|
|
364
|
+
"""Run a deep QE session (alias for 'qe run --mode deep').
|
|
365
|
+
|
|
366
|
+
Full investigation for pre-release and nightly CI.
|
|
367
|
+
"""
|
|
368
|
+
from superqode.superqe import QEOrchestrator
|
|
369
|
+
|
|
370
|
+
project_root = Path(path).resolve()
|
|
371
|
+
orchestrator = QEOrchestrator(project_root, verbose=True)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
result = asyncio.get_event_loop().run_until_complete(orchestrator.deep_qe())
|
|
375
|
+
return 0 if result.success else 1
|
|
376
|
+
except KeyboardInterrupt:
|
|
377
|
+
console.print("\n[yellow]Session cancelled[/yellow]")
|
|
378
|
+
return 130
|
|
379
|
+
except Exception as e:
|
|
380
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
381
|
+
return 1
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@qe.command("status")
|
|
385
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
386
|
+
def qe_status(path: str):
|
|
387
|
+
"""Show current QE workspace status."""
|
|
388
|
+
if not _enterprise_only("QE workspace status"):
|
|
389
|
+
return 1
|
|
390
|
+
from superqode.workspace import WorkspaceManager
|
|
391
|
+
|
|
392
|
+
project_root = Path(path).resolve()
|
|
393
|
+
workspace = WorkspaceManager(project_root)
|
|
394
|
+
|
|
395
|
+
console.print()
|
|
396
|
+
console.print(Panel("[bold]QE Workspace Status[/bold]", border_style="cyan"))
|
|
397
|
+
console.print()
|
|
398
|
+
|
|
399
|
+
# Check if .superqode exists
|
|
400
|
+
superqode_dir = project_root / ".superqode"
|
|
401
|
+
if not superqode_dir.exists():
|
|
402
|
+
console.print("[dim]No .superqode directory found.[/dim]")
|
|
403
|
+
console.print("[dim]Run 'superqe run .' to start a QE session.[/dim]")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
# Show state
|
|
407
|
+
state_file = superqode_dir / "workspace-state.json"
|
|
408
|
+
if state_file.exists():
|
|
409
|
+
import json
|
|
410
|
+
|
|
411
|
+
state = json.loads(state_file.read_text())
|
|
412
|
+
|
|
413
|
+
table = Table(show_header=False, box=None)
|
|
414
|
+
table.add_column("Key", style="dim")
|
|
415
|
+
table.add_column("Value")
|
|
416
|
+
|
|
417
|
+
table.add_row("State", state.get("state", "unknown"))
|
|
418
|
+
table.add_row("Session ID", state.get("session_id") or "-")
|
|
419
|
+
table.add_row("Started", state.get("session_start") or "-")
|
|
420
|
+
table.add_row("Updated", state.get("updated_at") or "-")
|
|
421
|
+
|
|
422
|
+
console.print(table)
|
|
423
|
+
|
|
424
|
+
# Show artifacts
|
|
425
|
+
artifacts_dir = superqode_dir / "qe-artifacts"
|
|
426
|
+
if artifacts_dir.exists():
|
|
427
|
+
console.print()
|
|
428
|
+
console.print("[bold]Artifacts:[/bold]")
|
|
429
|
+
|
|
430
|
+
manifest_file = artifacts_dir / "manifest.json"
|
|
431
|
+
if manifest_file.exists():
|
|
432
|
+
import json
|
|
433
|
+
|
|
434
|
+
manifest = json.loads(manifest_file.read_text())
|
|
435
|
+
|
|
436
|
+
by_type = {}
|
|
437
|
+
for artifact in manifest.get("artifacts", []):
|
|
438
|
+
t = artifact.get("type", "unknown")
|
|
439
|
+
by_type[t] = by_type.get(t, 0) + 1
|
|
440
|
+
|
|
441
|
+
for t, count in sorted(by_type.items()):
|
|
442
|
+
console.print(f" {t}: {count}")
|
|
443
|
+
|
|
444
|
+
# Show recent history
|
|
445
|
+
history_file = superqode_dir / "history" / "sessions.jsonl"
|
|
446
|
+
if history_file.exists():
|
|
447
|
+
console.print()
|
|
448
|
+
console.print("[bold]Recent Sessions:[/bold]")
|
|
449
|
+
|
|
450
|
+
sessions = []
|
|
451
|
+
with open(history_file) as f:
|
|
452
|
+
for line in f:
|
|
453
|
+
try:
|
|
454
|
+
sessions.append(__import__("json").loads(line))
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
for session in sessions[-5:]:
|
|
459
|
+
verdict = (
|
|
460
|
+
"✓"
|
|
461
|
+
if session.get("findings_count", 0) == 0
|
|
462
|
+
else f"⚠ {session.get('findings_count')} findings"
|
|
463
|
+
)
|
|
464
|
+
console.print(f" {session.get('session_id', 'unknown')}: {verdict}")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@qe.command("artifacts")
|
|
468
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
469
|
+
@click.option("--type", "-t", "artifact_type", help="Filter by type (patch, test_unit, qr, etc.)")
|
|
470
|
+
def qe_artifacts(path: str, artifact_type: str):
|
|
471
|
+
"""List QE artifacts from previous sessions."""
|
|
472
|
+
if not _enterprise_only("QE artifacts"):
|
|
473
|
+
return 1
|
|
474
|
+
from superqode.workspace.artifacts import ArtifactManager
|
|
475
|
+
|
|
476
|
+
project_root = Path(path).resolve()
|
|
477
|
+
manager = ArtifactManager(project_root)
|
|
478
|
+
manager.initialize("view")
|
|
479
|
+
|
|
480
|
+
artifacts = manager.get_all_artifacts()
|
|
481
|
+
|
|
482
|
+
if artifact_type:
|
|
483
|
+
artifacts = [a for a in artifacts if a.type.value == artifact_type]
|
|
484
|
+
|
|
485
|
+
if not artifacts:
|
|
486
|
+
console.print("[dim]No artifacts found.[/dim]")
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
console.print()
|
|
490
|
+
console.print(Panel("[bold]QE Artifacts[/bold]", border_style="cyan"))
|
|
491
|
+
console.print()
|
|
492
|
+
|
|
493
|
+
table = Table()
|
|
494
|
+
table.add_column("ID", style="cyan")
|
|
495
|
+
table.add_column("Type", style="magenta")
|
|
496
|
+
table.add_column("Name")
|
|
497
|
+
table.add_column("Description", style="dim")
|
|
498
|
+
|
|
499
|
+
for artifact in artifacts:
|
|
500
|
+
table.add_row(
|
|
501
|
+
artifact.id,
|
|
502
|
+
artifact.type.value,
|
|
503
|
+
artifact.name,
|
|
504
|
+
artifact.description[:40] + "..."
|
|
505
|
+
if len(artifact.description) > 40
|
|
506
|
+
else artifact.description,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
console.print(table)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@qe.command("show")
|
|
513
|
+
@click.argument("artifact_id")
|
|
514
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
515
|
+
def qe_show(artifact_id: str, path: str):
|
|
516
|
+
"""Show content of a specific artifact."""
|
|
517
|
+
if not _enterprise_only("QE artifact viewer"):
|
|
518
|
+
return 1
|
|
519
|
+
from superqode.workspace.artifacts import ArtifactManager
|
|
520
|
+
|
|
521
|
+
project_root = Path(path).resolve()
|
|
522
|
+
manager = ArtifactManager(project_root)
|
|
523
|
+
manager.initialize("view")
|
|
524
|
+
|
|
525
|
+
artifact = manager.get_artifact(artifact_id)
|
|
526
|
+
if not artifact:
|
|
527
|
+
console.print(f"[red]Artifact not found:[/red] {artifact_id}")
|
|
528
|
+
return 1
|
|
529
|
+
|
|
530
|
+
content = manager.get_artifact_content(artifact_id)
|
|
531
|
+
if not content:
|
|
532
|
+
console.print(f"[red]Could not read artifact content[/red]")
|
|
533
|
+
return 1
|
|
534
|
+
|
|
535
|
+
console.print()
|
|
536
|
+
console.print(
|
|
537
|
+
Panel(
|
|
538
|
+
f"[bold]{artifact.name}[/bold]\n"
|
|
539
|
+
f"[dim]Type: {artifact.type.value}[/dim]\n"
|
|
540
|
+
f"[dim]{artifact.description}[/dim]",
|
|
541
|
+
border_style="cyan",
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
console.print()
|
|
545
|
+
|
|
546
|
+
# Render based on type
|
|
547
|
+
if artifact.name.endswith(".md"):
|
|
548
|
+
console.print(Markdown(content))
|
|
549
|
+
elif artifact.name.endswith(".patch"):
|
|
550
|
+
console.print(content, highlight=True)
|
|
551
|
+
else:
|
|
552
|
+
console.print(content)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@qe.command("clean")
|
|
556
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
557
|
+
@click.option("--keep-qrs", is_flag=True, default=True, help="Keep QR files")
|
|
558
|
+
@click.option("--all", "clean_all", is_flag=True, help="Remove all including QRs")
|
|
559
|
+
@click.confirmation_option(prompt="Are you sure you want to clean artifacts?")
|
|
560
|
+
def qe_clean(path: str, keep_qrs: bool, clean_all: bool):
|
|
561
|
+
"""Clean up QE artifacts."""
|
|
562
|
+
if not _enterprise_only("QE artifact cleanup"):
|
|
563
|
+
return 1
|
|
564
|
+
from superqode.workspace.artifacts import ArtifactManager
|
|
565
|
+
|
|
566
|
+
project_root = Path(path).resolve()
|
|
567
|
+
manager = ArtifactManager(project_root)
|
|
568
|
+
manager.initialize("cleanup")
|
|
569
|
+
|
|
570
|
+
removed = manager.cleanup(keep_qrs=keep_qrs and not clean_all)
|
|
571
|
+
|
|
572
|
+
console.print(f"[green]✓[/green] Removed {removed} artifact(s)")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@qe.command("report")
|
|
576
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
577
|
+
@click.option(
|
|
578
|
+
"--format", "-f", type=click.Choice(["md", "json", "html"]), default="md", help="Output format"
|
|
579
|
+
)
|
|
580
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file path")
|
|
581
|
+
def qe_report(path: str, format: str, output: str):
|
|
582
|
+
"""View or export the latest QR."""
|
|
583
|
+
if not _enterprise_only("QE reports"):
|
|
584
|
+
return 1
|
|
585
|
+
from superqode.workspace.artifacts import ArtifactManager, ArtifactType
|
|
586
|
+
|
|
587
|
+
project_root = Path(path).resolve()
|
|
588
|
+
manager = ArtifactManager(project_root)
|
|
589
|
+
manager.initialize("view")
|
|
590
|
+
|
|
591
|
+
qrs = manager.list_qrs()
|
|
592
|
+
if not qrs:
|
|
593
|
+
console.print("[dim]No QR reports found.[/dim]")
|
|
594
|
+
console.print("[dim]Run 'superqe run .' to generate a report.[/dim]")
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
# Get latest QR
|
|
598
|
+
latest = qrs[-1]
|
|
599
|
+
content = manager.get_artifact_content(latest.id)
|
|
600
|
+
|
|
601
|
+
if output:
|
|
602
|
+
output_path = Path(output)
|
|
603
|
+
output_path.write_text(content)
|
|
604
|
+
console.print(f"[green]✓[/green] Report saved to {output_path}")
|
|
605
|
+
else:
|
|
606
|
+
console.print()
|
|
607
|
+
if format == "md":
|
|
608
|
+
console.print(Markdown(content))
|
|
609
|
+
else:
|
|
610
|
+
console.print(content)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _display_session_summary(result):
|
|
614
|
+
"""Display a summary of the completed QE session."""
|
|
615
|
+
from superqode.workspace.manager import QESessionResult
|
|
616
|
+
|
|
617
|
+
console.print()
|
|
618
|
+
console.print(Panel("[bold]QE Session Complete[/bold]", border_style="green"))
|
|
619
|
+
console.print()
|
|
620
|
+
|
|
621
|
+
# Verdict
|
|
622
|
+
if result.critical_count > 0:
|
|
623
|
+
verdict = "[red]🔴 FAIL - Critical issues found[/red]"
|
|
624
|
+
elif result.warning_count > 0:
|
|
625
|
+
verdict = "[yellow]🟡 CONDITIONAL PASS - Warnings found[/yellow]"
|
|
626
|
+
else:
|
|
627
|
+
verdict = "[green]🟢 PASS - No significant issues[/green]"
|
|
628
|
+
|
|
629
|
+
console.print(f"Verdict: {verdict}")
|
|
630
|
+
console.print()
|
|
631
|
+
|
|
632
|
+
# Summary table
|
|
633
|
+
table = Table(show_header=False, box=None)
|
|
634
|
+
table.add_column("Metric", style="dim")
|
|
635
|
+
table.add_column("Value")
|
|
636
|
+
|
|
637
|
+
table.add_row("Session ID", result.session_id)
|
|
638
|
+
table.add_row("Duration", f"{result.duration_seconds:.1f}s")
|
|
639
|
+
table.add_row("Mode", result.mode.value)
|
|
640
|
+
table.add_row("Findings", str(result.findings_count))
|
|
641
|
+
table.add_row(" Critical", str(result.critical_count))
|
|
642
|
+
table.add_row(" Warnings", str(result.warning_count))
|
|
643
|
+
table.add_row("Patches Generated", str(result.patches_generated))
|
|
644
|
+
table.add_row("Tests Generated", str(result.tests_generated))
|
|
645
|
+
table.add_row("Files Modified", str(len(result.files_modified)))
|
|
646
|
+
table.add_row("Files Created", str(len(result.files_created)))
|
|
647
|
+
table.add_row("Reverted", "✓" if result.reverted else "✗")
|
|
648
|
+
|
|
649
|
+
console.print(table)
|
|
650
|
+
console.print()
|
|
651
|
+
|
|
652
|
+
# Show artifact location
|
|
653
|
+
if result.qir_generated or result.patches_generated or result.tests_generated:
|
|
654
|
+
console.print("[dim]Artifacts saved to:[/dim] .superqode/qe-artifacts/")
|
|
655
|
+
console.print("[dim]View report with:[/dim] superqe report")
|
|
656
|
+
|
|
657
|
+
# Errors
|
|
658
|
+
if result.errors:
|
|
659
|
+
console.print()
|
|
660
|
+
console.print("[yellow]Errors:[/yellow]")
|
|
661
|
+
for error in result.errors:
|
|
662
|
+
console.print(f" • {error}")
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@qe.command("logs")
|
|
666
|
+
@click.argument("session_id", required=False)
|
|
667
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
668
|
+
def qe_logs(session_id: Optional[str], path: str):
|
|
669
|
+
"""Show detailed agent work logs for QE sessions.
|
|
670
|
+
|
|
671
|
+
Shows the actual agent interaction logs, including connection attempts,
|
|
672
|
+
prompts sent, responses received, and analysis steps. This provides
|
|
673
|
+
complete transparency into the AI analysis process.
|
|
674
|
+
|
|
675
|
+
If SESSION_ID is not provided, shows logs for the most recent session.
|
|
676
|
+
"""
|
|
677
|
+
if not _enterprise_only("QE logs"):
|
|
678
|
+
return 1
|
|
679
|
+
from superqode.workspace.artifacts import ArtifactManager
|
|
680
|
+
|
|
681
|
+
project_root = Path(path).resolve()
|
|
682
|
+
manager = ArtifactManager(project_root)
|
|
683
|
+
manager.initialize("qe_logs")
|
|
684
|
+
|
|
685
|
+
console.print()
|
|
686
|
+
console.print(Panel("[bold]AI Agent Work Logs[/bold]", border_style="blue"))
|
|
687
|
+
console.print()
|
|
688
|
+
|
|
689
|
+
# Find the session
|
|
690
|
+
superqode_dir = project_root / ".superqode"
|
|
691
|
+
if not superqode_dir.exists():
|
|
692
|
+
console.print("[red]No QE sessions found. Run 'superqe run .' first.[/red]")
|
|
693
|
+
return 1
|
|
694
|
+
|
|
695
|
+
# Get session ID if not provided
|
|
696
|
+
if not session_id:
|
|
697
|
+
# Find most recent QE agent log
|
|
698
|
+
qe_logs = manager.list_logs_by_type("qe_agent")
|
|
699
|
+
if qe_logs:
|
|
700
|
+
latest_log = max(qe_logs, key=lambda a: a.created_at)
|
|
701
|
+
console.print(f"[dim]Showing logs for latest QE agent session[/dim]")
|
|
702
|
+
console.print()
|
|
703
|
+
else:
|
|
704
|
+
# Fallback to finding most recent QR
|
|
705
|
+
qr_dir = superqode_dir / "qe-artifacts" / "qr"
|
|
706
|
+
if qr_dir.exists():
|
|
707
|
+
qr_files = list(qr_dir.glob("*.md"))
|
|
708
|
+
if qr_files:
|
|
709
|
+
latest_qr = max(qr_files, key=lambda f: f.stat().st_mtime)
|
|
710
|
+
console.print(
|
|
711
|
+
"[dim]No QE agent logs found, showing QR analysis for latest session[/dim]"
|
|
712
|
+
)
|
|
713
|
+
console.print()
|
|
714
|
+
_show_qr_work_logs(latest_qr)
|
|
715
|
+
return 0
|
|
716
|
+
|
|
717
|
+
console.print("[red]No QE sessions found with work logs.[/red]")
|
|
718
|
+
return 1
|
|
719
|
+
|
|
720
|
+
# Try to find QE agent logs for the session
|
|
721
|
+
qe_logs = manager.list_logs_by_type("qe_agent")
|
|
722
|
+
if qe_logs:
|
|
723
|
+
# Filter by session if provided
|
|
724
|
+
if session_id:
|
|
725
|
+
session_logs = [log for log in qe_logs if session_id in log.name]
|
|
726
|
+
else:
|
|
727
|
+
session_logs = qe_logs
|
|
728
|
+
|
|
729
|
+
if session_logs:
|
|
730
|
+
# Show the most recent log
|
|
731
|
+
latest_log = max(session_logs, key=lambda a: a.created_at)
|
|
732
|
+
|
|
733
|
+
console.print(f"[bold green]📋 QE Agent Session Log[/bold green]")
|
|
734
|
+
console.print(f"[dim]File: {latest_log.path}[/dim]")
|
|
735
|
+
console.print(
|
|
736
|
+
f"[dim]Created: {latest_log.created_at.strftime('%Y-%m-%d %H:%M:%S')}[/dim]"
|
|
737
|
+
)
|
|
738
|
+
console.print()
|
|
739
|
+
|
|
740
|
+
log_content = manager.get_artifact_content(latest_log.id)
|
|
741
|
+
if log_content:
|
|
742
|
+
# Display the log with syntax highlighting
|
|
743
|
+
console.print(log_content)
|
|
744
|
+
console.print()
|
|
745
|
+
console.print(
|
|
746
|
+
"[dim]💡 This log shows the complete agent interaction, including:[/dim]"
|
|
747
|
+
)
|
|
748
|
+
console.print("[dim] • Connection attempts and responses[/dim]")
|
|
749
|
+
console.print("[dim] • Prompts sent to the AI agent[/dim]")
|
|
750
|
+
console.print("[dim] • Analysis steps and reasoning[/dim]")
|
|
751
|
+
console.print("[dim] • Tool calls and their results[/dim]")
|
|
752
|
+
console.print("[dim] • Final findings extraction[/dim]")
|
|
753
|
+
else:
|
|
754
|
+
console.print("[red]Could not read log content[/red]")
|
|
755
|
+
return 1
|
|
756
|
+
else:
|
|
757
|
+
console.print(f"[yellow]No QE agent logs found for session {session_id}[/yellow]")
|
|
758
|
+
# Try to show QR work logs as fallback
|
|
759
|
+
_show_qr_work_logs_for_session(session_id, project_root)
|
|
760
|
+
else:
|
|
761
|
+
console.print("[yellow]No QE agent logs found[/yellow]")
|
|
762
|
+
console.print("[dim]QE agent logs are saved automatically during analysis.[/dim]")
|
|
763
|
+
# Try to show QR work logs as fallback
|
|
764
|
+
if session_id:
|
|
765
|
+
_show_qr_work_logs_for_session(session_id, project_root)
|
|
766
|
+
else:
|
|
767
|
+
# Find most recent QR
|
|
768
|
+
qr_dir = superqode_dir / "qe-artifacts" / "qr"
|
|
769
|
+
if qr_dir.exists():
|
|
770
|
+
qr_files = list(qr_dir.glob("*.md"))
|
|
771
|
+
if qr_files:
|
|
772
|
+
latest_qr = max(qr_files, key=lambda f: f.stat().st_mtime)
|
|
773
|
+
_show_qr_work_logs(latest_qr)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _show_qr_work_logs_for_session(session_id: str, project_root: Path):
|
|
777
|
+
"""Show work logs extracted from a QR for a specific session."""
|
|
778
|
+
superqode_dir = project_root / ".superqode"
|
|
779
|
+
qr_path = superqode_dir / "qe-artifacts" / "qr" / f"qr-*-qe-{session_id}.md"
|
|
780
|
+
qr_files = list(qr_path.parent.glob(qr_path.name.replace("*", "*")))
|
|
781
|
+
|
|
782
|
+
if qr_files:
|
|
783
|
+
_show_qr_work_logs(qr_files[0])
|
|
784
|
+
else:
|
|
785
|
+
console.print(f"[red]QR not found for session: {session_id}[/red]")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@qe.command("dashboard")
|
|
789
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
790
|
+
@click.option("--port", "-p", default=8765, help="Port for web server (default: 8765)")
|
|
791
|
+
@click.option("--no-open", is_flag=True, help="Don't open browser automatically")
|
|
792
|
+
@click.option("--export", "-e", type=click.Path(), help="Export as standalone HTML file")
|
|
793
|
+
def qe_dashboard(path: str, port: int, no_open: bool, export: str):
|
|
794
|
+
"""Open QR dashboard in web browser.
|
|
795
|
+
|
|
796
|
+
Provides an interactive web interface for viewing Quality Reports
|
|
797
|
+
with severity filtering, findings details, and verified fixes visualization.
|
|
798
|
+
|
|
799
|
+
Examples:
|
|
800
|
+
|
|
801
|
+
superqe dashboard # Open latest QR in browser
|
|
802
|
+
|
|
803
|
+
superqe dashboard --port 9000 # Use custom port
|
|
804
|
+
|
|
805
|
+
superqe dashboard --export report.html # Export as HTML file
|
|
806
|
+
"""
|
|
807
|
+
if not _enterprise_only("QE dashboard"):
|
|
808
|
+
return 1
|
|
809
|
+
from superqode.qr.dashboard import start_dashboard, find_latest_qr, export_html
|
|
810
|
+
|
|
811
|
+
project_root = Path(path).resolve()
|
|
812
|
+
|
|
813
|
+
# Find latest QR
|
|
814
|
+
qr_path = find_latest_qr(project_root)
|
|
815
|
+
if qr_path is None:
|
|
816
|
+
console.print("[red]No QR reports found.[/red]")
|
|
817
|
+
console.print("[dim]Run 'superqe run .' to generate a report first.[/dim]")
|
|
818
|
+
return 1
|
|
819
|
+
|
|
820
|
+
console.print(f"[dim]Using QR: {qr_path.name}[/dim]")
|
|
821
|
+
|
|
822
|
+
if export:
|
|
823
|
+
# Export mode
|
|
824
|
+
output_path = Path(export)
|
|
825
|
+
result_path = export_html(qr_path, output_path)
|
|
826
|
+
console.print(f"[green]✓[/green] Dashboard exported to {result_path}")
|
|
827
|
+
return 0
|
|
828
|
+
|
|
829
|
+
# Start web server
|
|
830
|
+
try:
|
|
831
|
+
start_dashboard(
|
|
832
|
+
qr_path=qr_path,
|
|
833
|
+
project_root=project_root,
|
|
834
|
+
port=port,
|
|
835
|
+
open_browser=not no_open,
|
|
836
|
+
)
|
|
837
|
+
except OSError as e:
|
|
838
|
+
if "Address already in use" in str(e):
|
|
839
|
+
console.print(f"[red]Port {port} is already in use.[/red]")
|
|
840
|
+
console.print(f"[dim]Try: superqe dashboard --port {port + 1}[/dim]")
|
|
841
|
+
else:
|
|
842
|
+
console.print(f"[red]Error starting dashboard: {e}[/red]")
|
|
843
|
+
return 1
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _show_qr_work_logs(qir_file: Path):
|
|
847
|
+
"""Show work logs extracted from a QR file."""
|
|
848
|
+
console.print("[bold yellow]📄 Analysis Summary from QR[/bold yellow]")
|
|
849
|
+
console.print(f"[dim]File: {qir_file}[/dim]")
|
|
850
|
+
console.print()
|
|
851
|
+
|
|
852
|
+
qir_content = qir_file.read_text()
|
|
853
|
+
|
|
854
|
+
# Extract work logs from QR
|
|
855
|
+
work_logs_found = False
|
|
856
|
+
current_section = None
|
|
857
|
+
in_analysis_process = False
|
|
858
|
+
in_evidence = False
|
|
859
|
+
|
|
860
|
+
for line in qir_content.split("\n"):
|
|
861
|
+
# Find finding sections
|
|
862
|
+
if line.startswith("### ") and ("🤖" in line or "🔍" in line or "✨" in line):
|
|
863
|
+
if current_section and work_logs_found:
|
|
864
|
+
console.print() # Add spacing between sections
|
|
865
|
+
current_section = line.replace("### ", "").replace("**", "")
|
|
866
|
+
console.print(f"[bold cyan]{current_section}[/bold cyan]")
|
|
867
|
+
work_logs_found = True
|
|
868
|
+
in_analysis_process = False
|
|
869
|
+
in_evidence = False
|
|
870
|
+
|
|
871
|
+
elif current_section:
|
|
872
|
+
if line.startswith("**Agent Analysis Process**:"):
|
|
873
|
+
in_analysis_process = True
|
|
874
|
+
in_evidence = False
|
|
875
|
+
console.print("[yellow]Agent Work Process:[/yellow]")
|
|
876
|
+
|
|
877
|
+
elif line.startswith("**Tools Used**:"):
|
|
878
|
+
in_evidence = False
|
|
879
|
+
in_analysis_process = False
|
|
880
|
+
tools = line.replace("**Tools Used**: ", "")
|
|
881
|
+
console.print(f"[green]🔧 Tools Used:[/green] {tools}")
|
|
882
|
+
|
|
883
|
+
elif (
|
|
884
|
+
in_analysis_process
|
|
885
|
+
and line.strip()
|
|
886
|
+
and not line.startswith("```")
|
|
887
|
+
and not line.startswith("... and")
|
|
888
|
+
):
|
|
889
|
+
console.print(f" {line}")
|
|
890
|
+
|
|
891
|
+
elif line.startswith("... and") and "more analysis steps" in line:
|
|
892
|
+
steps_match = line.split("... and ")[1].split(" more")[0]
|
|
893
|
+
console.print(f" [dim]... and {steps_match} more detailed steps[/dim]")
|
|
894
|
+
|
|
895
|
+
if not work_logs_found:
|
|
896
|
+
console.print("[yellow]No detailed work logs found in QR[/yellow]")
|
|
897
|
+
console.print(
|
|
898
|
+
"[dim]Work logs are available in QR reports for sessions with AI agent analysis.[/dim]"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
console.print()
|
|
902
|
+
console.print("[dim]💡 These logs show the analysis steps performed by AI agents,[/dim]")
|
|
903
|
+
console.print(
|
|
904
|
+
"[dim] demonstrating transparency and trustworthiness of the AI analysis.[/dim]"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@qe.command("feedback")
|
|
909
|
+
@click.argument("finding_id")
|
|
910
|
+
@click.option(
|
|
911
|
+
"--valid", "feedback_type", flag_value="valid", help="Mark finding as valid (true positive)"
|
|
912
|
+
)
|
|
913
|
+
@click.option(
|
|
914
|
+
"--false-positive",
|
|
915
|
+
"-fp",
|
|
916
|
+
"feedback_type",
|
|
917
|
+
flag_value="false_positive",
|
|
918
|
+
help="Mark finding as false positive (suppress in future)",
|
|
919
|
+
)
|
|
920
|
+
@click.option("--fixed", "feedback_type", flag_value="fixed", help="Mark finding as fixed")
|
|
921
|
+
@click.option(
|
|
922
|
+
"--wont-fix", "feedback_type", flag_value="wont_fix", help="Mark finding as won't fix"
|
|
923
|
+
)
|
|
924
|
+
@click.option("--reason", "-r", default="", help="Reason for the feedback")
|
|
925
|
+
@click.option(
|
|
926
|
+
"--scope",
|
|
927
|
+
"-s",
|
|
928
|
+
type=click.Choice(["project", "team"]),
|
|
929
|
+
default="project",
|
|
930
|
+
help="Scope for suppression (for false positives)",
|
|
931
|
+
)
|
|
932
|
+
@click.option(
|
|
933
|
+
"--expires",
|
|
934
|
+
"-e",
|
|
935
|
+
type=int,
|
|
936
|
+
default=None,
|
|
937
|
+
help="Suppression expires in N days (for false positives)",
|
|
938
|
+
)
|
|
939
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
940
|
+
def qe_feedback(
|
|
941
|
+
finding_id: str, feedback_type: str, reason: str, scope: str, expires: int, path: str
|
|
942
|
+
):
|
|
943
|
+
"""Provide feedback on a finding to improve future QE runs.
|
|
944
|
+
|
|
945
|
+
Feedback types:
|
|
946
|
+
- --valid: Confirm finding is a true positive
|
|
947
|
+
- --false-positive: Suppress this finding in future runs
|
|
948
|
+
- --fixed: Mark as fixed (can learn fix pattern)
|
|
949
|
+
- --wont-fix: Acknowledge but don't fix
|
|
950
|
+
|
|
951
|
+
Examples:
|
|
952
|
+
|
|
953
|
+
superqe feedback sec-001 --valid
|
|
954
|
+
|
|
955
|
+
superqe feedback sec-002 --false-positive -r "Intentional for testing"
|
|
956
|
+
|
|
957
|
+
superqe feedback sec-003 --false-positive --scope team -r "Known limitation"
|
|
958
|
+
|
|
959
|
+
superqe feedback perf-001 --fixed -r "Optimized query"
|
|
960
|
+
"""
|
|
961
|
+
if not _enterprise_only("QE feedback"):
|
|
962
|
+
return 1
|
|
963
|
+
from superqode.memory import FeedbackCollector, MemoryStore
|
|
964
|
+
|
|
965
|
+
if not feedback_type:
|
|
966
|
+
console.print("[red]Error:[/red] Must specify feedback type")
|
|
967
|
+
console.print("Options: --valid, --false-positive, --fixed, --wont-fix")
|
|
968
|
+
return 1
|
|
969
|
+
|
|
970
|
+
project_root = Path(path).resolve()
|
|
971
|
+
collector = FeedbackCollector(project_root)
|
|
972
|
+
|
|
973
|
+
# Find the finding in recent QRs
|
|
974
|
+
finding_info = _find_finding_in_qrs(project_root, finding_id)
|
|
975
|
+
if not finding_info:
|
|
976
|
+
console.print(f"[yellow]Warning:[/yellow] Finding '{finding_id}' not found in recent QRs")
|
|
977
|
+
console.print("[dim]Proceeding with limited information[/dim]")
|
|
978
|
+
finding_info = {
|
|
979
|
+
"id": finding_id,
|
|
980
|
+
"title": finding_id,
|
|
981
|
+
"fingerprint": None,
|
|
982
|
+
"category": "unknown",
|
|
983
|
+
"severity": "medium",
|
|
984
|
+
"found_by": "unknown",
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
console.print()
|
|
988
|
+
console.print(f"[bold]Finding:[/bold] {finding_info.get('title', finding_id)}")
|
|
989
|
+
console.print(f"[dim]ID: {finding_id}[/dim]")
|
|
990
|
+
console.print()
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
if feedback_type == "valid":
|
|
994
|
+
collector.mark_valid(
|
|
995
|
+
finding_id=finding_id,
|
|
996
|
+
finding_title=finding_info.get("title", finding_id),
|
|
997
|
+
category=finding_info.get("category", "unknown"),
|
|
998
|
+
severity=finding_info.get("severity", "medium"),
|
|
999
|
+
role_name=finding_info.get("found_by", "unknown"),
|
|
1000
|
+
reason=reason,
|
|
1001
|
+
)
|
|
1002
|
+
console.print("[green]✓[/green] Marked as valid (true positive)")
|
|
1003
|
+
|
|
1004
|
+
elif feedback_type == "false_positive":
|
|
1005
|
+
if not reason:
|
|
1006
|
+
console.print("[red]Error:[/red] Reason required for false positive")
|
|
1007
|
+
console.print("Use: --reason 'Your reason here'")
|
|
1008
|
+
return 1
|
|
1009
|
+
|
|
1010
|
+
feedback, suppression = collector.mark_false_positive(
|
|
1011
|
+
finding_id=finding_id,
|
|
1012
|
+
finding_title=finding_info.get("title", finding_id),
|
|
1013
|
+
finding_fingerprint=finding_info.get("fingerprint"),
|
|
1014
|
+
role_name=finding_info.get("found_by", "unknown"),
|
|
1015
|
+
reason=reason,
|
|
1016
|
+
scope=scope,
|
|
1017
|
+
expires_in_days=expires,
|
|
1018
|
+
)
|
|
1019
|
+
console.print("[green]✓[/green] Marked as false positive")
|
|
1020
|
+
console.print(f"[dim]Suppression created: {suppression.id}[/dim]")
|
|
1021
|
+
if scope == "team":
|
|
1022
|
+
console.print("[dim]Saved to .superqode/memory.json (commit to share)[/dim]")
|
|
1023
|
+
if expires:
|
|
1024
|
+
console.print(f"[dim]Expires in {expires} days[/dim]")
|
|
1025
|
+
|
|
1026
|
+
elif feedback_type == "fixed":
|
|
1027
|
+
collector.mark_fixed(
|
|
1028
|
+
finding_id=finding_id,
|
|
1029
|
+
finding_title=finding_info.get("title", finding_id),
|
|
1030
|
+
finding_fingerprint=finding_info.get("fingerprint"),
|
|
1031
|
+
fix_description=reason or "Fixed",
|
|
1032
|
+
)
|
|
1033
|
+
console.print("[green]✓[/green] Marked as fixed")
|
|
1034
|
+
|
|
1035
|
+
elif feedback_type == "wont_fix":
|
|
1036
|
+
collector.mark_wont_fix(
|
|
1037
|
+
finding_id=finding_id,
|
|
1038
|
+
finding_title=finding_info.get("title", finding_id),
|
|
1039
|
+
reason=reason or "Won't fix",
|
|
1040
|
+
)
|
|
1041
|
+
console.print("[green]✓[/green] Marked as won't fix")
|
|
1042
|
+
|
|
1043
|
+
console.print()
|
|
1044
|
+
console.print("[dim]Feedback recorded. Future QE runs will use this information.[/dim]")
|
|
1045
|
+
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
console.print(f"[red]Error recording feedback:[/red] {e}")
|
|
1048
|
+
return 1
|
|
1049
|
+
|
|
1050
|
+
return 0
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _find_finding_in_qrs(project_root: Path, finding_id: str) -> Optional[Dict]:
|
|
1054
|
+
"""Search recent QRs for a finding by ID."""
|
|
1055
|
+
qr_dir = project_root / ".superqode" / "qe-artifacts" / "qr"
|
|
1056
|
+
if not qr_dir.exists():
|
|
1057
|
+
return None
|
|
1058
|
+
|
|
1059
|
+
# Search JSON files
|
|
1060
|
+
for json_file in sorted(qr_dir.glob("*.json"), reverse=True)[:5]:
|
|
1061
|
+
try:
|
|
1062
|
+
data = json.loads(json_file.read_text())
|
|
1063
|
+
for finding in data.get("findings", []):
|
|
1064
|
+
if finding.get("id") == finding_id:
|
|
1065
|
+
return finding
|
|
1066
|
+
except Exception:
|
|
1067
|
+
continue
|
|
1068
|
+
|
|
1069
|
+
return None
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
@qe.command("suppressions")
|
|
1073
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
1074
|
+
@click.option("--remove", "-r", help="Remove suppression by ID")
|
|
1075
|
+
def qe_suppressions(path: str, remove: str):
|
|
1076
|
+
"""List or manage finding suppressions.
|
|
1077
|
+
|
|
1078
|
+
Suppressions prevent specific findings from appearing in future QE runs.
|
|
1079
|
+
They are created via 'superqe feedback --false-positive'.
|
|
1080
|
+
|
|
1081
|
+
Examples:
|
|
1082
|
+
|
|
1083
|
+
superqe suppressions # List active suppressions
|
|
1084
|
+
|
|
1085
|
+
superqe suppressions -r abc123 # Remove suppression by ID
|
|
1086
|
+
"""
|
|
1087
|
+
if not _enterprise_only("QE suppressions"):
|
|
1088
|
+
return 1
|
|
1089
|
+
from superqode.memory import MemoryStore
|
|
1090
|
+
|
|
1091
|
+
project_root = Path(path).resolve()
|
|
1092
|
+
store = MemoryStore(project_root)
|
|
1093
|
+
memory = store.load()
|
|
1094
|
+
|
|
1095
|
+
if remove:
|
|
1096
|
+
if store.remove_suppression(remove):
|
|
1097
|
+
console.print(f"[green]✓[/green] Removed suppression {remove}")
|
|
1098
|
+
else:
|
|
1099
|
+
console.print(f"[red]Suppression not found:[/red] {remove}")
|
|
1100
|
+
return
|
|
1101
|
+
|
|
1102
|
+
# List suppressions
|
|
1103
|
+
active = memory.get_active_suppressions()
|
|
1104
|
+
|
|
1105
|
+
console.print()
|
|
1106
|
+
console.print(Panel("[bold]Active Suppressions[/bold]", border_style="cyan"))
|
|
1107
|
+
console.print()
|
|
1108
|
+
|
|
1109
|
+
if not active:
|
|
1110
|
+
console.print("[dim]No active suppressions[/dim]")
|
|
1111
|
+
console.print()
|
|
1112
|
+
console.print("[dim]Create suppressions with:[/dim]")
|
|
1113
|
+
console.print(" superqe feedback <finding-id> --false-positive -r 'reason'")
|
|
1114
|
+
return
|
|
1115
|
+
|
|
1116
|
+
table = Table()
|
|
1117
|
+
table.add_column("ID", style="cyan")
|
|
1118
|
+
table.add_column("Pattern")
|
|
1119
|
+
table.add_column("Type")
|
|
1120
|
+
table.add_column("Scope")
|
|
1121
|
+
table.add_column("Reason", style="dim")
|
|
1122
|
+
table.add_column("Expires")
|
|
1123
|
+
|
|
1124
|
+
for supp in active:
|
|
1125
|
+
pattern_display = supp.pattern[:30] + "..." if len(supp.pattern) > 30 else supp.pattern
|
|
1126
|
+
expires = supp.expires_at[:10] if supp.expires_at else "-"
|
|
1127
|
+
table.add_row(
|
|
1128
|
+
supp.id,
|
|
1129
|
+
pattern_display,
|
|
1130
|
+
supp.pattern_type,
|
|
1131
|
+
supp.scope,
|
|
1132
|
+
supp.reason[:25] + "..." if len(supp.reason) > 25 else supp.reason,
|
|
1133
|
+
expires,
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
console.print(table)
|
|
1137
|
+
console.print()
|
|
1138
|
+
console.print(
|
|
1139
|
+
f"[dim]Total: {len(active)} active, {memory.total_suppressions_applied} applied[/dim]"
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _run_async(coro):
|
|
1144
|
+
"""Run a coroutine from sync CLI code with a compatible event loop."""
|
|
1145
|
+
return asyncio.run(coro)
|