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,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