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,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QE Coordinator - Session coordination with locking and epoch system.
|
|
3
|
+
|
|
4
|
+
Inspired by EveryCode's review_coord.rs implementation.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Lock-based coordination prevents concurrent deep QE runs
|
|
8
|
+
- Snapshot epochs detect if files changed during QE (stale results)
|
|
9
|
+
- Per-repo scoping using path hash
|
|
10
|
+
- Automatic cleanup of stale locks from dead processes
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
coordinator = QECoordinator(project_root)
|
|
14
|
+
|
|
15
|
+
# Try to acquire lock
|
|
16
|
+
lock = coordinator.acquire_lock("qe-session-001", mode="deep")
|
|
17
|
+
if lock is None:
|
|
18
|
+
print("Another QE session is running")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Run QE...
|
|
23
|
+
|
|
24
|
+
# Check if results are stale
|
|
25
|
+
if coordinator.is_result_stale(lock):
|
|
26
|
+
print("Warning: Code changed during QE run")
|
|
27
|
+
finally:
|
|
28
|
+
coordinator.release_lock(lock)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import signal
|
|
35
|
+
import subprocess
|
|
36
|
+
from dataclasses import dataclass, asdict
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any, Dict, Optional
|
|
40
|
+
import logging
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class QELock:
|
|
47
|
+
"""Lock information for a QE session."""
|
|
48
|
+
|
|
49
|
+
session_id: str
|
|
50
|
+
pid: int
|
|
51
|
+
started_at: str # ISO format
|
|
52
|
+
mode: str # "quick" or "deep"
|
|
53
|
+
git_head: Optional[str]
|
|
54
|
+
snapshot_epoch: int
|
|
55
|
+
intent: str # Description of the QE task
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: Dict[str, Any]) -> "QELock":
|
|
62
|
+
return cls(**data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class QECoordinator:
|
|
66
|
+
"""
|
|
67
|
+
Coordinate QE sessions to prevent conflicts and detect stale results.
|
|
68
|
+
|
|
69
|
+
Guarantees:
|
|
70
|
+
- Only one deep QE session at a time per repository
|
|
71
|
+
- Quick scans can run in parallel
|
|
72
|
+
- Detects if code changed during QE (stale results)
|
|
73
|
+
- Auto-cleanup of locks from dead processes
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
STATE_DIR = Path.home() / ".superqode" / "state" / "qe"
|
|
77
|
+
|
|
78
|
+
def __init__(self, project_root: Path):
|
|
79
|
+
self.project_root = project_root.resolve()
|
|
80
|
+
self._scope_dir: Optional[Path] = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def scope_key(self) -> str:
|
|
84
|
+
"""Get a unique key for this repository scope."""
|
|
85
|
+
# Use CRC32-like hash of path for uniqueness
|
|
86
|
+
path_bytes = str(self.project_root).encode()
|
|
87
|
+
return hashlib.md5(path_bytes).hexdigest()[:8]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def scope_dir(self) -> Path:
|
|
91
|
+
"""Get the state directory for this repository scope."""
|
|
92
|
+
if self._scope_dir is None:
|
|
93
|
+
state_dir = Path(os.environ.get("SUPERQODE_STATE_DIR", self.STATE_DIR))
|
|
94
|
+
self._scope_dir = state_dir / f"repo-{self.scope_key}"
|
|
95
|
+
self._scope_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
return self._scope_dir
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def lock_file(self) -> Path:
|
|
100
|
+
return self.scope_dir / "qe.lock"
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def epoch_file(self) -> Path:
|
|
104
|
+
return self.scope_dir / "snapshot.epoch"
|
|
105
|
+
|
|
106
|
+
# =========================================================================
|
|
107
|
+
# Epoch System - Detect file changes during QE
|
|
108
|
+
# =========================================================================
|
|
109
|
+
|
|
110
|
+
def get_snapshot_epoch(self) -> int:
|
|
111
|
+
"""Get the current snapshot epoch."""
|
|
112
|
+
if not self.epoch_file.exists():
|
|
113
|
+
return 0
|
|
114
|
+
try:
|
|
115
|
+
return int(self.epoch_file.read_text().strip())
|
|
116
|
+
except (ValueError, OSError):
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
def bump_snapshot_epoch(self) -> int:
|
|
120
|
+
"""
|
|
121
|
+
Increment the snapshot epoch.
|
|
122
|
+
|
|
123
|
+
Call this whenever files change (git operations, file edits, etc.)
|
|
124
|
+
"""
|
|
125
|
+
current = self.get_snapshot_epoch()
|
|
126
|
+
new_epoch = current + 1
|
|
127
|
+
self.epoch_file.write_text(str(new_epoch))
|
|
128
|
+
return new_epoch
|
|
129
|
+
|
|
130
|
+
def is_result_stale(self, lock: QELock) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Check if QE results are stale due to code changes.
|
|
133
|
+
|
|
134
|
+
Returns True if the snapshot epoch changed since the lock was acquired.
|
|
135
|
+
"""
|
|
136
|
+
current_epoch = self.get_snapshot_epoch()
|
|
137
|
+
return current_epoch > lock.snapshot_epoch
|
|
138
|
+
|
|
139
|
+
# =========================================================================
|
|
140
|
+
# Locking System - Coordinate QE sessions
|
|
141
|
+
# =========================================================================
|
|
142
|
+
|
|
143
|
+
def acquire_lock(
|
|
144
|
+
self,
|
|
145
|
+
session_id: str,
|
|
146
|
+
mode: str = "quick",
|
|
147
|
+
intent: str = "QE session",
|
|
148
|
+
) -> Optional[QELock]:
|
|
149
|
+
"""
|
|
150
|
+
Try to acquire a QE lock.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
session_id: Unique session identifier
|
|
154
|
+
mode: "quick" or "deep"
|
|
155
|
+
intent: Description of the QE task
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
QELock if acquired, None if another session holds the lock
|
|
159
|
+
"""
|
|
160
|
+
# Clean up stale locks first
|
|
161
|
+
self._clear_stale_locks()
|
|
162
|
+
|
|
163
|
+
# Check existing lock
|
|
164
|
+
existing = self.read_lock()
|
|
165
|
+
if existing is not None:
|
|
166
|
+
# Deep QE blocks everything
|
|
167
|
+
if existing.mode == "deep":
|
|
168
|
+
logger.info(f"Blocked by deep QE session: {existing.session_id}")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# New deep QE blocks if any session exists
|
|
172
|
+
if mode == "deep":
|
|
173
|
+
logger.info(f"Cannot start deep QE - session active: {existing.session_id}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# Create new lock
|
|
177
|
+
lock = QELock(
|
|
178
|
+
session_id=session_id,
|
|
179
|
+
pid=os.getpid(),
|
|
180
|
+
started_at=datetime.now().isoformat(),
|
|
181
|
+
mode=mode,
|
|
182
|
+
git_head=self._get_git_head(),
|
|
183
|
+
snapshot_epoch=self.get_snapshot_epoch(),
|
|
184
|
+
intent=intent,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Atomic write - create new file only
|
|
189
|
+
fd = os.open(
|
|
190
|
+
str(self.lock_file),
|
|
191
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
192
|
+
0o644,
|
|
193
|
+
)
|
|
194
|
+
with os.fdopen(fd, "w") as f:
|
|
195
|
+
json.dump(lock.to_dict(), f, indent=2)
|
|
196
|
+
|
|
197
|
+
logger.info(f"Acquired QE lock: {session_id} ({mode})")
|
|
198
|
+
return lock
|
|
199
|
+
|
|
200
|
+
except FileExistsError:
|
|
201
|
+
# Another process acquired the lock between check and create
|
|
202
|
+
logger.info("Lock acquisition race - another session won")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def release_lock(self, lock: QELock) -> None:
|
|
206
|
+
"""Release a QE lock."""
|
|
207
|
+
if not self.lock_file.exists():
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
current = self.read_lock()
|
|
212
|
+
if current and current.session_id == lock.session_id:
|
|
213
|
+
self.lock_file.unlink()
|
|
214
|
+
logger.info(f"Released QE lock: {lock.session_id}")
|
|
215
|
+
except OSError as e:
|
|
216
|
+
logger.warning(f"Failed to release lock: {e}")
|
|
217
|
+
|
|
218
|
+
def read_lock(self) -> Optional[QELock]:
|
|
219
|
+
"""Read the current lock if any."""
|
|
220
|
+
if not self.lock_file.exists():
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
data = json.loads(self.lock_file.read_text())
|
|
225
|
+
return QELock.from_dict(data)
|
|
226
|
+
except (json.JSONDecodeError, KeyError, OSError):
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _clear_stale_locks(self) -> bool:
|
|
230
|
+
"""
|
|
231
|
+
Remove stale locks from dead processes.
|
|
232
|
+
|
|
233
|
+
Returns True if a stale lock was cleared.
|
|
234
|
+
"""
|
|
235
|
+
lock = self.read_lock()
|
|
236
|
+
if lock is None:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
if self._is_process_alive(lock.pid):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Process is dead - remove stale lock
|
|
243
|
+
try:
|
|
244
|
+
self.lock_file.unlink()
|
|
245
|
+
logger.info(f"Cleared stale lock from dead process: PID {lock.pid}")
|
|
246
|
+
return True
|
|
247
|
+
except OSError:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def _is_process_alive(self, pid: int) -> bool:
|
|
251
|
+
"""Check if a process is still running."""
|
|
252
|
+
try:
|
|
253
|
+
# Signal 0 checks if process exists without sending signal
|
|
254
|
+
os.kill(pid, 0)
|
|
255
|
+
return True
|
|
256
|
+
except ProcessLookupError:
|
|
257
|
+
return False
|
|
258
|
+
except PermissionError:
|
|
259
|
+
# Process exists but we don't have permission
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
def _get_git_head(self) -> Optional[str]:
|
|
263
|
+
"""Get the current git HEAD commit."""
|
|
264
|
+
try:
|
|
265
|
+
result = subprocess.run(
|
|
266
|
+
["git", "rev-parse", "HEAD"],
|
|
267
|
+
cwd=str(self.project_root),
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
timeout=5,
|
|
271
|
+
)
|
|
272
|
+
if result.returncode == 0:
|
|
273
|
+
return result.stdout.strip()
|
|
274
|
+
except (subprocess.SubprocessError, OSError):
|
|
275
|
+
pass
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
# =========================================================================
|
|
279
|
+
# Context Manager Support
|
|
280
|
+
# =========================================================================
|
|
281
|
+
|
|
282
|
+
def session(
|
|
283
|
+
self,
|
|
284
|
+
session_id: str,
|
|
285
|
+
mode: str = "quick",
|
|
286
|
+
intent: str = "QE session",
|
|
287
|
+
) -> "QESessionContext":
|
|
288
|
+
"""
|
|
289
|
+
Context manager for QE sessions.
|
|
290
|
+
|
|
291
|
+
Usage:
|
|
292
|
+
with coordinator.session("my-session", mode="deep") as lock:
|
|
293
|
+
if lock:
|
|
294
|
+
# Run QE...
|
|
295
|
+
"""
|
|
296
|
+
return QESessionContext(self, session_id, mode, intent)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class QESessionContext:
|
|
300
|
+
"""Context manager for QE sessions with automatic lock management."""
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
coordinator: QECoordinator,
|
|
305
|
+
session_id: str,
|
|
306
|
+
mode: str,
|
|
307
|
+
intent: str,
|
|
308
|
+
):
|
|
309
|
+
self.coordinator = coordinator
|
|
310
|
+
self.session_id = session_id
|
|
311
|
+
self.mode = mode
|
|
312
|
+
self.intent = intent
|
|
313
|
+
self.lock: Optional[QELock] = None
|
|
314
|
+
|
|
315
|
+
def __enter__(self) -> Optional[QELock]:
|
|
316
|
+
self.lock = self.coordinator.acquire_lock(
|
|
317
|
+
self.session_id,
|
|
318
|
+
self.mode,
|
|
319
|
+
self.intent,
|
|
320
|
+
)
|
|
321
|
+
return self.lock
|
|
322
|
+
|
|
323
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
324
|
+
if self.lock:
|
|
325
|
+
self.coordinator.release_lock(self.lock)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# =============================================================================
|
|
329
|
+
# Global Epoch Notification
|
|
330
|
+
# =============================================================================
|
|
331
|
+
|
|
332
|
+
_global_coordinator: Optional[QECoordinator] = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def set_global_coordinator(coordinator: QECoordinator) -> None:
|
|
336
|
+
"""Set the global coordinator for epoch notifications."""
|
|
337
|
+
global _global_coordinator
|
|
338
|
+
_global_coordinator = coordinator
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def notify_file_change() -> None:
|
|
342
|
+
"""
|
|
343
|
+
Notify that files have changed.
|
|
344
|
+
|
|
345
|
+
Call this from file write operations to update the epoch.
|
|
346
|
+
"""
|
|
347
|
+
if _global_coordinator:
|
|
348
|
+
_global_coordinator.bump_snapshot_epoch()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def get_global_coordinator() -> Optional[QECoordinator]:
|
|
352
|
+
"""Get the global coordinator if set."""
|
|
353
|
+
return _global_coordinator
|