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,373 @@
1
+ """
2
+ Git Guard - Prevents Git Operations During QE Sessions.
3
+
4
+ Ensures the immutable repo guarantee by blocking all git operations
5
+ that could permanently alter the repository state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import List, Optional, Set, Tuple
14
+
15
+
16
+ class GitOperationType(Enum):
17
+ """Types of git operations."""
18
+
19
+ READ = "read" # Safe: status, log, diff, show, branch -l
20
+ WRITE = "write" # Blocked: add, commit, push, merge, rebase
21
+ DESTRUCTIVE = "destructive" # Blocked: reset --hard, clean -f, checkout -f
22
+
23
+
24
+ class GitOperationBlocked(Exception):
25
+ """Raised when a blocked git operation is attempted."""
26
+
27
+ def __init__(self, command: str, reason: str, suggestion: str = ""):
28
+ self.command = command
29
+ self.reason = reason
30
+ self.suggestion = suggestion
31
+ super().__init__(f"Git operation blocked: {reason}")
32
+
33
+
34
+ @dataclass
35
+ class GitCommandAnalysis:
36
+ """Analysis of a git command."""
37
+
38
+ command: str
39
+ operation_type: GitOperationType
40
+ is_blocked: bool
41
+ reason: str
42
+ suggestion: str = ""
43
+
44
+
45
+ class GitGuard:
46
+ """
47
+ Guards against git operations that would violate the immutable repo guarantee.
48
+
49
+ Rules:
50
+ - ❌ No commits
51
+ - ❌ No pushes
52
+ - ❌ No branching/merging/rebasing
53
+ - ❌ No checkout that overwrites changes
54
+ - ❌ No reset that loses changes
55
+ - ❌ No clean that removes files
56
+ - ✅ Read operations allowed (status, log, diff, show)
57
+
58
+ Usage:
59
+ guard = GitGuard()
60
+
61
+ # Check before executing
62
+ if guard.is_blocked("git commit -m 'test'"):
63
+ raise guard.analyze("git commit -m 'test'").reason
64
+
65
+ # Or use the wrapper
66
+ guard.check_command("git push origin main") # Raises GitOperationBlocked
67
+ """
68
+
69
+ # Git commands that are always safe (read-only)
70
+ SAFE_COMMANDS: Set[str] = {
71
+ "status",
72
+ "log",
73
+ "diff",
74
+ "show",
75
+ "branch",
76
+ "tag",
77
+ "ls-files",
78
+ "ls-tree",
79
+ "cat-file",
80
+ "rev-parse",
81
+ "describe",
82
+ "name-rev",
83
+ "shortlog",
84
+ "whatchanged",
85
+ "blame",
86
+ "annotate",
87
+ "grep",
88
+ "log",
89
+ "reflog",
90
+ "remote",
91
+ "config",
92
+ "help",
93
+ "version",
94
+ }
95
+
96
+ # Git commands that are blocked (write operations)
97
+ BLOCKED_COMMANDS: Set[str] = {
98
+ "commit",
99
+ "push",
100
+ "pull",
101
+ "fetch",
102
+ "merge",
103
+ "rebase",
104
+ "cherry-pick",
105
+ "revert",
106
+ "reset",
107
+ "checkout",
108
+ "switch",
109
+ "restore",
110
+ "add",
111
+ "rm",
112
+ "mv",
113
+ "clean",
114
+ "stash",
115
+ "tag",
116
+ "branch",
117
+ "remote",
118
+ "submodule",
119
+ "subtree",
120
+ "init",
121
+ "clone",
122
+ "gc",
123
+ "prune",
124
+ "fsck",
125
+ "reflog",
126
+ }
127
+
128
+ # Patterns for safe variants of normally blocked commands
129
+ SAFE_PATTERNS: List[Tuple[str, re.Pattern]] = [
130
+ # git branch -l, --list, -a, -r (listing only)
131
+ ("branch", re.compile(r"branch\s+(-[lar]+|--list|--all|--remotes)(\s|$)")),
132
+ # git remote -v, show, get-url (listing only)
133
+ ("remote", re.compile(r"remote\s+(-v|--verbose|show|get-url)(\s|$)")),
134
+ # git tag -l, --list (listing only)
135
+ ("tag", re.compile(r"tag\s+(-l|--list)(\s|$)")),
136
+ # git stash list, show (reading only)
137
+ ("stash", re.compile(r"stash\s+(list|show)(\s|$)")),
138
+ # git config --get, --list (reading only)
139
+ ("config", re.compile(r"config\s+(--get|--list|-l)(\s|$)")),
140
+ # git diff (always safe)
141
+ ("diff", re.compile(r"diff(\s|$)")),
142
+ # git log (always safe)
143
+ ("log", re.compile(r"log(\s|$)")),
144
+ # git status (always safe)
145
+ ("status", re.compile(r"status(\s|$)")),
146
+ # git show (always safe)
147
+ ("show", re.compile(r"show(\s|$)")),
148
+ ]
149
+
150
+ # Human-readable reasons for blocking
151
+ BLOCK_REASONS = {
152
+ "commit": "Commits would permanently alter the repository history",
153
+ "push": "Push would send changes to remote repository",
154
+ "pull": "Pull could introduce external changes during QE session",
155
+ "fetch": "Fetch is unnecessary during ephemeral QE session",
156
+ "merge": "Merge would alter branch history",
157
+ "rebase": "Rebase would rewrite commit history",
158
+ "cherry-pick": "Cherry-pick would create new commits",
159
+ "revert": "Revert would create new commits",
160
+ "reset": "Reset could lose tracked changes",
161
+ "checkout": "Checkout could overwrite working changes",
162
+ "switch": "Branch switching is not allowed during QE",
163
+ "restore": "Restore could overwrite working changes",
164
+ "add": "Staging changes is not needed in ephemeral workspace",
165
+ "rm": "Git rm would stage deletions",
166
+ "mv": "Git mv would stage renames",
167
+ "clean": "Git clean could remove untracked files",
168
+ "stash": "Stashing is not needed in ephemeral workspace",
169
+ "tag": "Creating tags is not allowed during QE",
170
+ "branch": "Creating/deleting branches is not allowed during QE",
171
+ "init": "Repository initialization is not allowed",
172
+ "clone": "Cloning is not allowed during QE session",
173
+ }
174
+
175
+ SUGGESTIONS = {
176
+ "commit": "Changes are automatically tracked and reverted. Use QIR to document findings.",
177
+ "push": "All findings are saved to .superqode/qe-artifacts/ for review.",
178
+ "add": "File tracking is automatic in ephemeral workspace.",
179
+ "checkout": "File modifications are tracked and will be reverted automatically.",
180
+ "reset": "Use 'superqode revert' to manually revert specific changes.",
181
+ "clean": "Ephemeral files are cleaned up automatically after QE session.",
182
+ "stash": "All changes are ephemeral - no need to stash.",
183
+ "branch": "QE runs in ephemeral mode - no branch needed.",
184
+ }
185
+
186
+ def __init__(self, enabled: bool = True):
187
+ """
188
+ Initialize the Git Guard.
189
+
190
+ Args:
191
+ enabled: If False, guard is disabled (all operations allowed).
192
+ """
193
+ self.enabled = enabled
194
+ self._blocked_attempts: List[GitCommandAnalysis] = []
195
+
196
+ def is_git_command(self, command: str) -> bool:
197
+ """Check if a command is a git command."""
198
+ cmd = command.strip().lower()
199
+ return cmd.startswith("git ") or cmd == "git"
200
+
201
+ def extract_git_subcommand(self, command: str) -> Optional[str]:
202
+ """Extract the git subcommand from a full command."""
203
+ parts = command.strip().split()
204
+ if len(parts) < 2:
205
+ return None
206
+ if parts[0].lower() != "git":
207
+ return None
208
+ return parts[1].lower()
209
+
210
+ def is_safe_variant(self, command: str, subcommand: str) -> bool:
211
+ """Check if this is a safe variant of a normally blocked command."""
212
+ # Remove 'git ' prefix for pattern matching
213
+ cmd_without_git = (
214
+ command.strip()[4:].strip() if command.strip().lower().startswith("git ") else command
215
+ )
216
+
217
+ for pattern_cmd, pattern in self.SAFE_PATTERNS:
218
+ if subcommand == pattern_cmd and pattern.search(cmd_without_git):
219
+ return True
220
+ return False
221
+
222
+ def analyze(self, command: str) -> GitCommandAnalysis:
223
+ """
224
+ Analyze a git command and determine if it should be blocked.
225
+
226
+ Returns detailed analysis including reason and suggestion.
227
+ """
228
+ if not self.is_git_command(command):
229
+ return GitCommandAnalysis(
230
+ command=command,
231
+ operation_type=GitOperationType.READ,
232
+ is_blocked=False,
233
+ reason="Not a git command",
234
+ )
235
+
236
+ subcommand = self.extract_git_subcommand(command)
237
+
238
+ if not subcommand:
239
+ return GitCommandAnalysis(
240
+ command=command,
241
+ operation_type=GitOperationType.READ,
242
+ is_blocked=False,
243
+ reason="Bare git command",
244
+ )
245
+
246
+ # Check if it's a known safe command
247
+ if subcommand in self.SAFE_COMMANDS:
248
+ return GitCommandAnalysis(
249
+ command=command,
250
+ operation_type=GitOperationType.READ,
251
+ is_blocked=False,
252
+ reason=f"'{subcommand}' is a read-only operation",
253
+ )
254
+
255
+ # Check if it's a safe variant of a blocked command
256
+ if subcommand in self.BLOCKED_COMMANDS and self.is_safe_variant(command, subcommand):
257
+ return GitCommandAnalysis(
258
+ command=command,
259
+ operation_type=GitOperationType.READ,
260
+ is_blocked=False,
261
+ reason=f"'{subcommand}' in read-only mode",
262
+ )
263
+
264
+ # Check if it's a blocked command
265
+ if subcommand in self.BLOCKED_COMMANDS:
266
+ reason = self.BLOCK_REASONS.get(
267
+ subcommand, f"'{subcommand}' could modify repository state"
268
+ )
269
+ suggestion = self.SUGGESTIONS.get(subcommand, "")
270
+
271
+ # Determine operation type
272
+ if subcommand in {"reset", "clean", "checkout"}:
273
+ op_type = GitOperationType.DESTRUCTIVE
274
+ else:
275
+ op_type = GitOperationType.WRITE
276
+
277
+ return GitCommandAnalysis(
278
+ command=command,
279
+ operation_type=op_type,
280
+ is_blocked=True,
281
+ reason=reason,
282
+ suggestion=suggestion,
283
+ )
284
+
285
+ # Unknown git command - block by default for safety
286
+ return GitCommandAnalysis(
287
+ command=command,
288
+ operation_type=GitOperationType.WRITE,
289
+ is_blocked=True,
290
+ reason=f"Unknown git subcommand '{subcommand}' - blocked for safety",
291
+ suggestion="Only read operations (status, log, diff, show) are allowed during QE.",
292
+ )
293
+
294
+ def is_blocked(self, command: str) -> bool:
295
+ """Quick check if a command is blocked."""
296
+ if not self.enabled:
297
+ return False
298
+ if not self.is_git_command(command):
299
+ return False
300
+ return self.analyze(command).is_blocked
301
+
302
+ def check_command(self, command: str) -> None:
303
+ """
304
+ Check a command and raise GitOperationBlocked if blocked.
305
+
306
+ Use this as a guard before executing commands.
307
+ """
308
+ if not self.enabled:
309
+ return
310
+
311
+ analysis = self.analyze(command)
312
+
313
+ if analysis.is_blocked:
314
+ self._blocked_attempts.append(analysis)
315
+ raise GitOperationBlocked(
316
+ command=command,
317
+ reason=analysis.reason,
318
+ suggestion=analysis.suggestion,
319
+ )
320
+
321
+ def get_blocked_attempts(self) -> List[GitCommandAnalysis]:
322
+ """Get list of all blocked command attempts."""
323
+ return self._blocked_attempts.copy()
324
+
325
+ def clear_blocked_attempts(self) -> None:
326
+ """Clear the blocked attempts log."""
327
+ self._blocked_attempts.clear()
328
+
329
+ def format_block_message(self, analysis: GitCommandAnalysis) -> str:
330
+ """Format a user-friendly block message."""
331
+ lines = [
332
+ "🛡️ Git Operation Blocked",
333
+ "━" * 40,
334
+ f"Command: {analysis.command}",
335
+ f"Reason: {analysis.reason}",
336
+ ]
337
+
338
+ if analysis.suggestion:
339
+ lines.append(f"💡 Tip: {analysis.suggestion}")
340
+
341
+ lines.extend(
342
+ [
343
+ "",
344
+ "SuperQode runs in ephemeral mode - all changes are",
345
+ "automatically tracked and reverted after QE completes.",
346
+ "Findings are preserved in .superqode/qe-artifacts/",
347
+ ]
348
+ )
349
+
350
+ return "\n".join(lines)
351
+
352
+
353
+ # Singleton instance for easy access
354
+ _default_guard: Optional[GitGuard] = None
355
+
356
+
357
+ def get_git_guard() -> GitGuard:
358
+ """Get the default Git Guard instance."""
359
+ global _default_guard
360
+ if _default_guard is None:
361
+ _default_guard = GitGuard()
362
+ return _default_guard
363
+
364
+
365
+ def set_git_guard(guard: GitGuard) -> None:
366
+ """Set the default Git Guard instance."""
367
+ global _default_guard
368
+ _default_guard = guard
369
+
370
+
371
+ def check_git_command(command: str) -> None:
372
+ """Convenience function to check a command against the default guard."""
373
+ get_git_guard().check_command(command)