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