superqode 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)