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,750 @@
1
+ """
2
+ Workspace Manager - Ephemeral-Edit Workspace with Immutable Repo Guarantee.
3
+
4
+ The core orchestrator for SuperQode's QE sessions:
5
+ - Agents can freely modify/generate code
6
+ - All changes are tracked and reverted after session
7
+ - Artifacts (patches, tests, QIRs) are preserved
8
+ - Git operations are blocked
9
+
10
+ Usage:
11
+ workspace = WorkspaceManager(project_root)
12
+
13
+ async with workspace.qe_session("my-session") as session:
14
+ # Agents can now modify files freely
15
+ # Changes tracked, git blocked
16
+ await run_qe_agents()
17
+
18
+ # Session ends: all changes reverted, artifacts preserved
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ from contextlib import asynccontextmanager
25
+ from dataclasses import dataclass, field
26
+ from datetime import datetime
27
+ from enum import Enum
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Set
30
+ import json
31
+ import logging
32
+
33
+ from .snapshot import SnapshotManager
34
+ from .artifacts import ArtifactManager, ArtifactType, Artifact
35
+ from .git_guard import GitGuard, GitOperationBlocked, check_git_command
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class WorkspaceState(Enum):
41
+ """State of the workspace."""
42
+
43
+ IDLE = "idle" # No active session
44
+ ACTIVE = "active" # QE session in progress
45
+ REVERTING = "reverting" # Reverting changes
46
+ PRESERVING = "preserving" # Preserving artifacts
47
+ ERROR = "error" # Error state
48
+
49
+
50
+ class QEMode(Enum):
51
+ """QE execution mode."""
52
+
53
+ QUICK_SCAN = "quick_scan" # Fast, shallow, time-boxed
54
+ DEEP_QE = "deep_qe" # Full exploration, destructive allowed
55
+
56
+
57
+ @dataclass
58
+ class QESessionConfig:
59
+ """Configuration for a QE session."""
60
+
61
+ mode: QEMode = QEMode.QUICK_SCAN
62
+ timeout_seconds: int = 60 # Quick scan default
63
+ destructive_allowed: bool = False # Can run stress tests etc.
64
+ generate_tests: bool = True # Generate new tests
65
+ generate_patches: bool = True # Generate fix suggestions
66
+ roles: List[str] = field(default_factory=list) # QE roles to run
67
+
68
+
69
+ @dataclass
70
+ class QESessionResult:
71
+ """Result of a QE session."""
72
+
73
+ session_id: str
74
+ mode: QEMode
75
+ started_at: datetime
76
+ ended_at: datetime
77
+ duration_seconds: float
78
+
79
+ # Changes tracking
80
+ files_modified: List[str]
81
+ files_created: List[str]
82
+ files_deleted: List[str]
83
+
84
+ # Artifacts
85
+ patches_generated: int
86
+ tests_generated: int
87
+ qir_generated: bool
88
+ artifact_summary: Dict[str, int]
89
+
90
+ # Findings
91
+ findings_count: int
92
+ critical_count: int
93
+ warning_count: int
94
+
95
+ # Status
96
+ reverted: bool
97
+ errors: List[str]
98
+
99
+ def to_dict(self) -> Dict[str, Any]:
100
+ """Serialize to dictionary."""
101
+ return {
102
+ "session_id": self.session_id,
103
+ "mode": self.mode.value,
104
+ "started_at": self.started_at.isoformat(),
105
+ "ended_at": self.ended_at.isoformat(),
106
+ "duration_seconds": self.duration_seconds,
107
+ "files_modified": self.files_modified,
108
+ "files_created": self.files_created,
109
+ "files_deleted": self.files_deleted,
110
+ "patches_generated": self.patches_generated,
111
+ "tests_generated": self.tests_generated,
112
+ "qir_generated": self.qir_generated,
113
+ "artifact_summary": self.artifact_summary,
114
+ "findings_count": self.findings_count,
115
+ "critical_count": self.critical_count,
116
+ "warning_count": self.warning_count,
117
+ "reverted": self.reverted,
118
+ "errors": self.errors,
119
+ }
120
+
121
+
122
+ @dataclass
123
+ class Finding:
124
+ """A finding from QE analysis."""
125
+
126
+ id: str
127
+ severity: str # "critical", "warning", "info"
128
+ title: str
129
+ description: str
130
+ file_path: Optional[str] = None
131
+ line_number: Optional[int] = None
132
+ evidence: Optional[str] = None
133
+ suggested_fix: Optional[str] = None
134
+ patch_artifact_id: Optional[str] = None
135
+ work_log: Optional[List[str]] = None
136
+ tool_calls: Optional[List[str]] = None
137
+
138
+
139
+ class WorkspaceManager:
140
+ """
141
+ Manages the ephemeral-edit workspace for QE sessions.
142
+
143
+ Guarantees:
144
+ - ❌ No commits
145
+ - ❌ No pushes
146
+ - ❌ No git operations (branching, merges, tagging)
147
+ - ✅ All changes reverted after session
148
+ - ✅ Artifacts preserved in .superqode/qe-artifacts/
149
+ """
150
+
151
+ SUPERQODE_DIR = ".superqode"
152
+ STATE_FILE = "workspace-state.json"
153
+
154
+ def __init__(self, project_root: Path):
155
+ self.project_root = project_root.resolve()
156
+ self.superqode_dir = self.project_root / self.SUPERQODE_DIR
157
+
158
+ # Components
159
+ self.snapshot = SnapshotManager(self.project_root)
160
+ self.artifacts = ArtifactManager(self.project_root)
161
+ self.git_guard = GitGuard(enabled=True)
162
+
163
+ # Session state
164
+ self._state = WorkspaceState.IDLE
165
+ self._session_id: Optional[str] = None
166
+ self._session_start: Optional[datetime] = None
167
+ self._session_config: Optional[QESessionConfig] = None
168
+ self._findings: List[Finding] = []
169
+ self._finding_counter = 0
170
+
171
+ @property
172
+ def state(self) -> WorkspaceState:
173
+ """Current workspace state."""
174
+ return self._state
175
+
176
+ @property
177
+ def session_id(self) -> Optional[str]:
178
+ """Current session ID if active."""
179
+ return self._session_id
180
+
181
+ @property
182
+ def is_active(self) -> bool:
183
+ """Check if a QE session is active."""
184
+ return self._state == WorkspaceState.ACTIVE
185
+
186
+ def initialize(self) -> None:
187
+ """Initialize the .superqode directory structure."""
188
+ self.superqode_dir.mkdir(parents=True, exist_ok=True)
189
+
190
+ # Create subdirectories
191
+ for subdir in ["qe-artifacts", "config", "history", "temp"]:
192
+ (self.superqode_dir / subdir).mkdir(exist_ok=True)
193
+
194
+ # Create .gitignore to exclude temp files
195
+ gitignore_path = self.superqode_dir / ".gitignore"
196
+ if not gitignore_path.exists():
197
+ gitignore_path.write_text("# SuperQode temporary files\ntemp/\n*.tmp\n*.log\n")
198
+
199
+ def start_session(
200
+ self,
201
+ session_id: Optional[str] = None,
202
+ config: Optional[QESessionConfig] = None,
203
+ ) -> str:
204
+ """
205
+ Start a new QE session.
206
+
207
+ Args:
208
+ session_id: Optional session ID (auto-generated if not provided)
209
+ config: Session configuration
210
+
211
+ Returns:
212
+ Session ID
213
+ """
214
+ if self._state != WorkspaceState.IDLE:
215
+ raise RuntimeError(f"Cannot start session: workspace in {self._state.value} state")
216
+
217
+ # Initialize directories
218
+ self.initialize()
219
+
220
+ # Generate session ID
221
+ self._session_id = session_id or f"qe-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
222
+ self._session_start = datetime.now()
223
+ self._session_config = config or QESessionConfig()
224
+ self._findings.clear()
225
+ self._finding_counter = 0
226
+
227
+ # Start snapshot tracking
228
+ self.snapshot.start_session(self._session_id)
229
+
230
+ # Initialize artifacts
231
+ self.artifacts.initialize(self._session_id)
232
+
233
+ # Clear git guard attempts
234
+ self.git_guard.clear_blocked_attempts()
235
+
236
+ # Update state
237
+ self._state = WorkspaceState.ACTIVE
238
+ self._save_state()
239
+
240
+ logger.info(f"Started QE session: {self._session_id}")
241
+
242
+ return self._session_id
243
+
244
+ def end_session(self, generate_qir: bool = True) -> QESessionResult:
245
+ """
246
+ End the current QE session.
247
+
248
+ - Generates QIR if requested
249
+ - Reverts all file changes
250
+ - Preserves artifacts
251
+
252
+ Returns:
253
+ QESessionResult with session summary
254
+ """
255
+ if self._state != WorkspaceState.ACTIVE:
256
+ raise RuntimeError(f"No active session to end (state: {self._state.value})")
257
+
258
+ session_end = datetime.now()
259
+ errors = []
260
+
261
+ # Get changes before reverting
262
+ changes = self.snapshot.get_changes_summary()
263
+
264
+ # Generate QIR if requested
265
+ qir_generated = False
266
+ if generate_qir:
267
+ try:
268
+ self._state = WorkspaceState.PRESERVING
269
+ self._generate_qir()
270
+ qir_generated = True
271
+ except Exception as e:
272
+ errors.append(f"QIR generation failed: {e}")
273
+ logger.error(f"QIR generation failed: {e}")
274
+
275
+ # Revert all changes
276
+ self._state = WorkspaceState.REVERTING
277
+ try:
278
+ revert_result = self.snapshot.end_session(revert=True)
279
+ reverted = True
280
+ except Exception as e:
281
+ errors.append(f"Revert failed: {e}")
282
+ logger.error(f"Revert failed: {e}")
283
+ reverted = False
284
+
285
+ # Get artifact summary
286
+ artifact_summary = self.artifacts.get_summary()
287
+
288
+ # Build result
289
+ result = QESessionResult(
290
+ session_id=self._session_id,
291
+ mode=self._session_config.mode,
292
+ started_at=self._session_start,
293
+ ended_at=session_end,
294
+ duration_seconds=(session_end - self._session_start).total_seconds(),
295
+ files_modified=changes.get("files_modified", []),
296
+ files_created=changes.get("files_created", []),
297
+ files_deleted=changes.get("files_deleted", []),
298
+ patches_generated=len(self.artifacts.list_patches()),
299
+ tests_generated=len(self.artifacts.list_generated_tests()),
300
+ qir_generated=qir_generated,
301
+ artifact_summary=artifact_summary.get("by_type", {}),
302
+ findings_count=len(self._findings),
303
+ critical_count=sum(1 for f in self._findings if f.severity == "critical"),
304
+ warning_count=sum(1 for f in self._findings if f.severity == "warning"),
305
+ reverted=reverted,
306
+ errors=errors,
307
+ )
308
+
309
+ # Save result to history
310
+ self._save_session_result(result)
311
+
312
+ # Reset state
313
+ self._state = WorkspaceState.IDLE
314
+ self._session_id = None
315
+ self._session_start = None
316
+ self._session_config = None
317
+ self._save_state()
318
+
319
+ logger.info(f"Ended QE session: {result.session_id}")
320
+
321
+ return result
322
+
323
+ @asynccontextmanager
324
+ async def qe_session(
325
+ self,
326
+ session_id: Optional[str] = None,
327
+ config: Optional[QESessionConfig] = None,
328
+ ):
329
+ """
330
+ Context manager for QE sessions.
331
+
332
+ Usage:
333
+ async with workspace.qe_session() as session:
334
+ # Do QE work
335
+ pass
336
+ # Automatically reverted, artifacts preserved
337
+ """
338
+ sid = self.start_session(session_id, config)
339
+ try:
340
+ yield self
341
+ finally:
342
+ self.end_session(generate_qir=True)
343
+
344
+ # =========================================================================
345
+ # File Operations (with tracking)
346
+ # =========================================================================
347
+
348
+ def read_file(self, file_path: str) -> str:
349
+ """Read a file (no tracking needed for reads)."""
350
+ abs_path = self.project_root / file_path
351
+ return abs_path.read_text()
352
+
353
+ def write_file(self, file_path: str, content: str) -> None:
354
+ """
355
+ Write to a file (tracked for reversion).
356
+
357
+ Captures original state before first write.
358
+ """
359
+ if not self.is_active:
360
+ raise RuntimeError("No active QE session - cannot write files")
361
+
362
+ # Capture original state before modification
363
+ self.snapshot.capture_file(Path(file_path))
364
+
365
+ # Create parent directories if needed
366
+ abs_path = self.project_root / file_path
367
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
368
+
369
+ # Write the file
370
+ abs_path.write_text(content)
371
+
372
+ # Record the modification
373
+ self.snapshot.record_modification(Path(file_path))
374
+
375
+ def delete_file(self, file_path: str) -> None:
376
+ """
377
+ Delete a file (tracked for reversion).
378
+ """
379
+ if not self.is_active:
380
+ raise RuntimeError("No active QE session - cannot delete files")
381
+
382
+ # Capture original state
383
+ self.snapshot.capture_file(Path(file_path))
384
+
385
+ # Delete the file
386
+ abs_path = self.project_root / file_path
387
+ if abs_path.exists():
388
+ abs_path.unlink()
389
+ self.snapshot.record_deletion(Path(file_path))
390
+
391
+ def check_command(self, command: str) -> None:
392
+ """
393
+ Check if a shell command is allowed.
394
+
395
+ Raises GitOperationBlocked for blocked git operations.
396
+ """
397
+ self.git_guard.check_command(command)
398
+
399
+ # =========================================================================
400
+ # Findings
401
+ # =========================================================================
402
+
403
+ def add_finding(
404
+ self,
405
+ severity: str,
406
+ title: str,
407
+ description: str,
408
+ file_path: Optional[str] = None,
409
+ line_number: Optional[int] = None,
410
+ evidence: Optional[str] = None,
411
+ suggested_fix: Optional[str] = None,
412
+ work_log: Optional[List[str]] = None,
413
+ tool_calls: Optional[List[str]] = None,
414
+ ) -> Finding:
415
+ """Add a finding from QE analysis."""
416
+ self._finding_counter += 1
417
+ finding = Finding(
418
+ id=f"finding-{self._finding_counter:03d}",
419
+ severity=severity,
420
+ title=title,
421
+ description=description,
422
+ file_path=file_path,
423
+ line_number=line_number,
424
+ evidence=evidence,
425
+ suggested_fix=suggested_fix,
426
+ work_log=work_log,
427
+ tool_calls=tool_calls,
428
+ )
429
+ self._findings.append(finding)
430
+
431
+ # If there's a suggested fix, create a patch
432
+ if suggested_fix and file_path:
433
+ try:
434
+ original = self.snapshot.get_original_content(Path(file_path))
435
+ if original:
436
+ artifact = self.artifacts.save_patch(
437
+ original_file=file_path,
438
+ original_content=original.decode("utf-8"),
439
+ modified_content=suggested_fix,
440
+ description=title,
441
+ )
442
+ finding.patch_artifact_id = artifact.id
443
+ except Exception as e:
444
+ logger.warning(f"Failed to create patch artifact: {e}")
445
+
446
+ return finding
447
+
448
+ def get_findings(self, severity: Optional[str] = None) -> List[Finding]:
449
+ """Get findings, optionally filtered by severity."""
450
+ if severity:
451
+ return [f for f in self._findings if f.severity == severity]
452
+ return self._findings.copy()
453
+
454
+ # =========================================================================
455
+ # Artifacts
456
+ # =========================================================================
457
+
458
+ def save_generated_test(
459
+ self,
460
+ test_type: str,
461
+ filename: str,
462
+ content: str,
463
+ description: str = "",
464
+ target_file: Optional[str] = None,
465
+ ) -> Artifact:
466
+ """Save a generated test file to artifacts."""
467
+ type_map = {
468
+ "unit": ArtifactType.TEST_UNIT,
469
+ "integration": ArtifactType.TEST_INTEGRATION,
470
+ "api": ArtifactType.TEST_API,
471
+ "contract": ArtifactType.TEST_CONTRACT,
472
+ "fuzz": ArtifactType.TEST_FUZZ,
473
+ "load": ArtifactType.TEST_LOAD,
474
+ "regression": ArtifactType.TEST_REGRESSION,
475
+ "e2e": ArtifactType.TEST_E2E,
476
+ "security": ArtifactType.TEST_SECURITY,
477
+ }
478
+
479
+ artifact_type = type_map.get(test_type.lower(), ArtifactType.TEST_UNIT)
480
+
481
+ return self.artifacts.save_generated_test(
482
+ test_type=artifact_type,
483
+ filename=filename,
484
+ content=content,
485
+ description=description,
486
+ target_file=target_file,
487
+ )
488
+
489
+ def save_patch(
490
+ self,
491
+ original_file: str,
492
+ original_content: str,
493
+ modified_content: str,
494
+ description: str = "",
495
+ ) -> Artifact:
496
+ """Save a patch file to artifacts."""
497
+ return self.artifacts.save_patch(
498
+ original_file=original_file,
499
+ original_content=original_content,
500
+ modified_content=modified_content,
501
+ description=description,
502
+ )
503
+
504
+ # =========================================================================
505
+ # QR Generation
506
+ # =========================================================================
507
+
508
+ def _generate_qir(self) -> Artifact:
509
+ """Generate the Quality Report (QR)."""
510
+ changes = self.snapshot.get_changes_summary()
511
+
512
+ # Build QR content
513
+ lines = [
514
+ "# Quality Report (QR)",
515
+ "",
516
+ f"**Session ID**: `{self._session_id}`",
517
+ f"**Mode**: {self._session_config.mode.value}",
518
+ f"**Started**: {self._session_start.isoformat()}",
519
+ f"**Duration**: {(datetime.now() - self._session_start).total_seconds():.1f}s",
520
+ "",
521
+ ]
522
+
523
+ # Executive Summary
524
+ critical_count = sum(1 for f in self._findings if f.severity == "critical")
525
+ warning_count = sum(1 for f in self._findings if f.severity == "warning")
526
+ info_count = sum(1 for f in self._findings if f.severity == "info")
527
+
528
+ if critical_count > 0:
529
+ verdict = "🔴 **FAIL** - Critical issues found"
530
+ elif warning_count > 0:
531
+ verdict = "🟡 **CONDITIONAL PASS** - Warnings found"
532
+ else:
533
+ verdict = "🟢 **PASS** - No significant issues"
534
+
535
+ lines.extend(
536
+ [
537
+ "## Executive Summary",
538
+ "",
539
+ f"**Verdict**: {verdict}",
540
+ "",
541
+ f"| Severity | Count |",
542
+ f"|----------|-------|",
543
+ f"| 🔴 Critical | {critical_count} |",
544
+ f"| 🟡 Warning | {warning_count} |",
545
+ f"| 🔵 Info | {info_count} |",
546
+ "",
547
+ ]
548
+ )
549
+
550
+ # Scope
551
+ lines.extend(
552
+ [
553
+ "## Investigation Scope",
554
+ "",
555
+ f"- Files analyzed: {changes.get('files_tracked', 0)}",
556
+ f"- Files modified during QE: {len(changes.get('files_modified', []))}",
557
+ f"- Files created during QE: {len(changes.get('files_created', []))}",
558
+ "",
559
+ ]
560
+ )
561
+
562
+ # Findings
563
+ if self._findings:
564
+ lines.extend(
565
+ [
566
+ "## Findings",
567
+ "",
568
+ ]
569
+ )
570
+
571
+ for finding in self._findings:
572
+ severity_icon = {"critical": "🔴", "warning": "🟡", "info": "🔵"}.get(
573
+ finding.severity, "⚪"
574
+ )
575
+ lines.append(f"### {severity_icon} {finding.title}")
576
+ lines.append("")
577
+
578
+ if finding.file_path:
579
+ location = finding.file_path
580
+ if finding.line_number:
581
+ location += f":{finding.line_number}"
582
+ lines.append(f"**Location**: `{location}`")
583
+ lines.append("")
584
+
585
+ lines.append(finding.description)
586
+ lines.append("")
587
+
588
+ if finding.evidence:
589
+ lines.append("**Evidence**:")
590
+ lines.append("```")
591
+ lines.append(finding.evidence)
592
+ lines.append("```")
593
+ lines.append("")
594
+
595
+ # Include work log if available (from session findings)
596
+ if hasattr(finding, "work_log") and finding.work_log:
597
+ lines.append("**Agent Analysis Process**:")
598
+ lines.append("```")
599
+ for step in finding.work_log[:5]: # Show first 5 steps to keep QIR concise
600
+ lines.append(step)
601
+ if len(finding.work_log) > 5:
602
+ lines.append(f"... and {len(finding.work_log) - 5} more analysis steps")
603
+ lines.append("```")
604
+ lines.append("")
605
+
606
+ # Include tool calls if available
607
+ if hasattr(finding, "tool_calls") and finding.tool_calls:
608
+ lines.append(f"**Tools Used**: {', '.join(finding.tool_calls)}")
609
+ lines.append("")
610
+
611
+ if finding.patch_artifact_id:
612
+ lines.append(f"**Suggested Fix**: See `{finding.patch_artifact_id}`")
613
+ lines.append("")
614
+ else:
615
+ lines.extend(
616
+ [
617
+ "## Findings",
618
+ "",
619
+ "No issues found during this QE session.",
620
+ "",
621
+ ]
622
+ )
623
+
624
+ # Generated Artifacts
625
+ patches = self.artifacts.list_patches()
626
+ tests = self.artifacts.list_generated_tests()
627
+
628
+ if patches or tests:
629
+ lines.extend(
630
+ [
631
+ "## Generated Artifacts",
632
+ "",
633
+ ]
634
+ )
635
+
636
+ if patches:
637
+ lines.append("### Patches")
638
+ lines.append("")
639
+ for patch in patches:
640
+ lines.append(f"- `{patch.name}`: {patch.description}")
641
+ lines.append("")
642
+
643
+ if tests:
644
+ lines.append("### Generated Tests")
645
+ lines.append("")
646
+ for test in tests:
647
+ lines.append(f"- `{test.name}` ({test.type.value}): {test.description}")
648
+ lines.append("")
649
+
650
+ # Git Operations Blocked
651
+ blocked = self.git_guard.get_blocked_attempts()
652
+ if blocked:
653
+ lines.extend(
654
+ [
655
+ "## Blocked Operations",
656
+ "",
657
+ "The following git operations were blocked to maintain repo integrity:",
658
+ "",
659
+ ]
660
+ )
661
+ for attempt in blocked:
662
+ lines.append(f"- `{attempt.command}`: {attempt.reason}")
663
+ lines.append("")
664
+
665
+ # Footer
666
+ lines.extend(
667
+ [
668
+ "---",
669
+ "",
670
+ "*Generated by SuperQode - Agentic Quality Engineering*",
671
+ "",
672
+ f"All changes have been reverted. Artifacts preserved in `.superqode/qe-artifacts/`",
673
+ ]
674
+ )
675
+
676
+ content = "\n".join(lines)
677
+
678
+ # Save QIR
679
+ metadata = {
680
+ "session_id": self._session_id,
681
+ "mode": self._session_config.mode.value,
682
+ "findings_count": len(self._findings),
683
+ "critical_count": critical_count,
684
+ "warning_count": warning_count,
685
+ "patches_count": len(patches),
686
+ "tests_count": len(tests),
687
+ }
688
+
689
+ return self.artifacts.save_qir(content, self._session_id, metadata)
690
+
691
+ # =========================================================================
692
+ # State Management
693
+ # =========================================================================
694
+
695
+ def _save_state(self) -> None:
696
+ """Save current state to file."""
697
+ state_file = self.superqode_dir / self.STATE_FILE
698
+ state_file.parent.mkdir(parents=True, exist_ok=True)
699
+
700
+ state = {
701
+ "state": self._state.value,
702
+ "session_id": self._session_id,
703
+ "session_start": self._session_start.isoformat() if self._session_start else None,
704
+ "updated_at": datetime.now().isoformat(),
705
+ }
706
+
707
+ state_file.write_text(json.dumps(state, indent=2))
708
+
709
+ def _save_session_result(self, result: QESessionResult) -> None:
710
+ """Save session result to history."""
711
+ history_file = self.superqode_dir / "history" / "sessions.jsonl"
712
+ history_file.parent.mkdir(parents=True, exist_ok=True)
713
+
714
+ with open(history_file, "a") as f:
715
+ f.write(json.dumps(result.to_dict()) + "\n")
716
+
717
+ def get_session_history(self, limit: int = 10) -> List[Dict[str, Any]]:
718
+ """Get recent session history."""
719
+ history_file = self.superqode_dir / "history" / "sessions.jsonl"
720
+ if not history_file.exists():
721
+ return []
722
+
723
+ sessions = []
724
+ with open(history_file) as f:
725
+ for line in f:
726
+ try:
727
+ sessions.append(json.loads(line))
728
+ except json.JSONDecodeError:
729
+ continue
730
+
731
+ return sessions[-limit:]
732
+
733
+
734
+ # Global workspace instance
735
+ _workspace: Optional[WorkspaceManager] = None
736
+
737
+
738
+ def get_workspace(project_root: Optional[Path] = None) -> WorkspaceManager:
739
+ """Get or create the global workspace manager."""
740
+ global _workspace
741
+ if _workspace is None:
742
+ root = project_root or Path.cwd()
743
+ _workspace = WorkspaceManager(root)
744
+ return _workspace
745
+
746
+
747
+ def set_workspace(workspace: WorkspaceManager) -> None:
748
+ """Set the global workspace manager."""
749
+ global _workspace
750
+ _workspace = workspace