ata-coder 2.4.2__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.
- ata_coder/__init__.py +1 -0
- ata_coder/agent.py +874 -0
- ata_coder/agent_compact.py +190 -0
- ata_coder/agent_controller.py +218 -0
- ata_coder/agent_extension.py +69 -0
- ata_coder/agent_routing.py +105 -0
- ata_coder/agent_subsystems.py +72 -0
- ata_coder/agent_tools.py +318 -0
- ata_coder/agent_undo.py +63 -0
- ata_coder/anthropic_client.py +465 -0
- ata_coder/change_tracker.py +368 -0
- ata_coder/clawd_integration.py +574 -0
- ata_coder/commands/__init__.py +128 -0
- ata_coder/commands/_core.py +184 -0
- ata_coder/commands/_safety.py +95 -0
- ata_coder/commands/_settings.py +241 -0
- ata_coder/commands/_workflow.py +451 -0
- ata_coder/commands.py +974 -0
- ata_coder/config.py +257 -0
- ata_coder/core/__init__.py +35 -0
- ata_coder/core/events.py +73 -0
- ata_coder/core/queue.py +85 -0
- ata_coder/core/state.py +17 -0
- ata_coder/event_queue.py +5 -0
- ata_coder/extension.py +654 -0
- ata_coder/extensions/__init__.py +1 -0
- ata_coder/extensions/hello_skill.py +47 -0
- ata_coder/fool_proof.py +295 -0
- ata_coder/git_workflow.py +371 -0
- ata_coder/gui.py +511 -0
- ata_coder/llm_client.py +543 -0
- ata_coder/main.py +814 -0
- ata_coder/mcp_client.py +1095 -0
- ata_coder/memory.py +539 -0
- ata_coder/model_registry.py +134 -0
- ata_coder/model_router.py +105 -0
- ata_coder/permissions.py +274 -0
- ata_coder/privilege.py +464 -0
- ata_coder/project.py +273 -0
- ata_coder/prompt_template.py +423 -0
- ata_coder/prompts/auto-mode.md +7 -0
- ata_coder/prompts/coding-rules.md +40 -0
- ata_coder/prompts/execution-guardrails.md +14 -0
- ata_coder/prompts/memory-system.md +24 -0
- ata_coder/prompts/output-style.md +23 -0
- ata_coder/prompts/safety.md +17 -0
- ata_coder/prompts/slash-commands.md +24 -0
- ata_coder/prompts/sub-agents.md +38 -0
- ata_coder/prompts/system-reminders.md +17 -0
- ata_coder/prompts/system.md +105 -0
- ata_coder/prompts/tool-policy.md +46 -0
- ata_coder/repl_theme.py +99 -0
- ata_coder/repl_tracker.py +89 -0
- ata_coder/repl_ui.py +1214 -0
- ata_coder/safety_guard.py +434 -0
- ata_coder/self_correct.py +346 -0
- ata_coder/server.py +882 -0
- ata_coder/server_session.py +159 -0
- ata_coder/server_shell.py +129 -0
- ata_coder/session.py +431 -0
- ata_coder/settings.py +439 -0
- ata_coder/setup_wizard.py +136 -0
- ata_coder/skill_extension.py +92 -0
- ata_coder/skills/architect/SKILL.md +42 -0
- ata_coder/skills/code-reviewer/SKILL.md +37 -0
- ata_coder/skills/codecraft/SKILL.md +452 -0
- ata_coder/skills/debugger/SKILL.md +45 -0
- ata_coder/skills/doc-writer/SKILL.md +36 -0
- ata_coder/skills/general-coder/SKILL.md +76 -0
- ata_coder/skills/math-calculator/README.md +40 -0
- ata_coder/skills/math-calculator/SKILL.md +59 -0
- ata_coder/skills/math-calculator/handler.py +103 -0
- ata_coder/skills/math-calculator/prompts/system.md +8 -0
- ata_coder/skills/math-calculator/requirements.txt +2 -0
- ata_coder/skills/math-calculator/resources/constants.json +8 -0
- ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
- ata_coder/skills/security-auditor/SKILL.md +40 -0
- ata_coder/skills/test-writer/SKILL.md +36 -0
- ata_coder/skills/weather-skill/README.md +45 -0
- ata_coder/skills/weather-skill/handler.py +76 -0
- ata_coder/skills/weather-skill/manifest.json +48 -0
- ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
- ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
- ata_coder/skills/weather-skill/requirements.txt +1 -0
- ata_coder/skills/weather-skill/resources/city_list.json +17 -0
- ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
- ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
- ata_coder/skills/weather-skill/weather_utils.py +50 -0
- ata_coder/skills.py +1014 -0
- ata_coder/sub_agent.py +273 -0
- ata_coder/sub_agent_manager.py +203 -0
- ata_coder/system_prompt_builder.py +146 -0
- ata_coder/task_planner.py +391 -0
- ata_coder/terminal.py +318 -0
- ata_coder/test_runner.py +219 -0
- ata_coder/thread_supervisor.py +195 -0
- ata_coder/tool_defs.py +335 -0
- ata_coder/tools/__init__.py +11 -0
- ata_coder/tools/definitions.py +335 -0
- ata_coder/tools/executor.py +1036 -0
- ata_coder/tools/result.py +26 -0
- ata_coder/tools/subagent.py +332 -0
- ata_coder/tools/web.py +361 -0
- ata_coder/tools.py +1576 -0
- ata_coder/types.py +92 -0
- ata_coder/utils.py +113 -0
- ata_coder/web/css/style.css +180 -0
- ata_coder/web/index.html +84 -0
- ata_coder/web/js/app.js +489 -0
- ata_coder/web/package-lock.json +25 -0
- ata_coder/web/package.json +10 -0
- ata_coder/web/tsconfig.json +13 -0
- ata_coder-2.4.2.dist-info/METADATA +799 -0
- ata_coder-2.4.2.dist-info/RECORD +118 -0
- ata_coder-2.4.2.dist-info/WHEEL +5 -0
- ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
- ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
- ata_coder-2.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Workflow — integrated version control for AI-assisted coding.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Auto-create feature branches for tasks
|
|
6
|
+
- Auto-commit after successful changes with meaningful messages
|
|
7
|
+
- Pre-commit safety checks (no secrets, no giant files)
|
|
8
|
+
- Git status in UI
|
|
9
|
+
- PR/merge request creation hints
|
|
10
|
+
- Stash management for interrupted work
|
|
11
|
+
|
|
12
|
+
Commands (via CLI):
|
|
13
|
+
/git status → Show working tree status
|
|
14
|
+
/git diff → Show unstaged changes
|
|
15
|
+
/git commit → Auto-commit with generated message
|
|
16
|
+
/git branch <name> → Create and switch to feature branch
|
|
17
|
+
/git undo-commit → Undo last commit (soft reset)
|
|
18
|
+
/git log [n] → Show recent commits
|
|
19
|
+
|
|
20
|
+
Safety:
|
|
21
|
+
- Never force-push
|
|
22
|
+
- Never amend pushed commits
|
|
23
|
+
- Auto-stash before dangerous operations
|
|
24
|
+
- Warn on large diffs (>500 lines)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
# Helpers
|
|
40
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
def _run_git(args: list[str], cwd: str | Path, timeout: int = 30) -> tuple[int, str, str]:
|
|
43
|
+
"""Run a git command. Returns (returncode, stdout, stderr)."""
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["git"] + args,
|
|
47
|
+
capture_output=True, text=True,
|
|
48
|
+
encoding="utf-8", errors="replace",
|
|
49
|
+
cwd=str(cwd), timeout=timeout,
|
|
50
|
+
)
|
|
51
|
+
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
return -1, "", "git not found"
|
|
54
|
+
except subprocess.TimeoutExpired:
|
|
55
|
+
return -1, "", "timeout"
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return -1, "", str(e)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_git_repo(cwd: str | Path) -> bool:
|
|
61
|
+
code, _, _ = _run_git(["rev-parse", "--git-dir"], cwd)
|
|
62
|
+
return code == 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
# Git Workflow Manager
|
|
67
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class GitStatus:
|
|
71
|
+
branch: str = ""
|
|
72
|
+
clean: bool = True
|
|
73
|
+
staged: int = 0
|
|
74
|
+
modified: int = 0
|
|
75
|
+
untracked: int = 0
|
|
76
|
+
ahead: int = 0
|
|
77
|
+
behind: int = 0
|
|
78
|
+
last_commit: str = ""
|
|
79
|
+
last_commit_msg: str = ""
|
|
80
|
+
|
|
81
|
+
def summary(self) -> str:
|
|
82
|
+
if self.clean:
|
|
83
|
+
return f"[{self.branch}] clean"
|
|
84
|
+
parts = []
|
|
85
|
+
if self.modified:
|
|
86
|
+
parts.append(f"M:{self.modified}")
|
|
87
|
+
if self.staged:
|
|
88
|
+
parts.append(f"S:{self.staged}")
|
|
89
|
+
if self.untracked:
|
|
90
|
+
parts.append(f"?:{self.untracked}")
|
|
91
|
+
return f"[{self.branch}] " + " ".join(parts)
|
|
92
|
+
|
|
93
|
+
def is_dirty(self) -> bool:
|
|
94
|
+
return not self.clean
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GitWorkflow:
|
|
98
|
+
"""
|
|
99
|
+
Git workflow manager for AI-assisted development.
|
|
100
|
+
|
|
101
|
+
Provides safe, automated git operations with guard rails.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, cwd: str | Path | None = None):
|
|
105
|
+
self.cwd = Path(cwd) if cwd else Path.cwd()
|
|
106
|
+
self._commits_made: list[str] = [] # track commits made in this session
|
|
107
|
+
self._branches_created: list[str] = []
|
|
108
|
+
|
|
109
|
+
# ── Status ──────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def get_status(self) -> GitStatus:
|
|
112
|
+
"""Get the current git working tree status."""
|
|
113
|
+
if not is_git_repo(self.cwd):
|
|
114
|
+
return GitStatus(branch="(not a git repo)")
|
|
115
|
+
|
|
116
|
+
status = GitStatus()
|
|
117
|
+
|
|
118
|
+
# Branch
|
|
119
|
+
_, branch, _ = _run_git(["branch", "--show-current"], self.cwd)
|
|
120
|
+
status.branch = branch or "(detached)"
|
|
121
|
+
|
|
122
|
+
# Short status
|
|
123
|
+
code, output, _ = _run_git(["status", "--short"], self.cwd)
|
|
124
|
+
if code == 0 and output:
|
|
125
|
+
for line in output.split("\n"):
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if not line:
|
|
128
|
+
continue
|
|
129
|
+
if line.startswith("?"):
|
|
130
|
+
status.untracked += 1
|
|
131
|
+
elif line[0] in "MADRC":
|
|
132
|
+
status.staged += 1
|
|
133
|
+
elif len(line) > 1 and line[1] in "MD":
|
|
134
|
+
status.modified += 1
|
|
135
|
+
status.clean = (status.staged == 0 and status.modified == 0 and status.untracked == 0)
|
|
136
|
+
|
|
137
|
+
# Ahead/behind
|
|
138
|
+
_, ahead_str, _ = _run_git(["rev-list", "--count", "@{u}..HEAD"], self.cwd, timeout=10)
|
|
139
|
+
if ahead_str and ahead_str.isdigit():
|
|
140
|
+
status.ahead = int(ahead_str)
|
|
141
|
+
_, behind_str, _ = _run_git(["rev-list", "--count", "HEAD..@{u}"], self.cwd, timeout=10)
|
|
142
|
+
if behind_str and behind_str.isdigit():
|
|
143
|
+
status.behind = int(behind_str)
|
|
144
|
+
|
|
145
|
+
# Last commit
|
|
146
|
+
_, last, _ = _run_git(["log", "-1", "--format=%h %s"], self.cwd)
|
|
147
|
+
if last:
|
|
148
|
+
parts = last.split(" ", 1)
|
|
149
|
+
status.last_commit = parts[0]
|
|
150
|
+
status.last_commit_msg = parts[1] if len(parts) > 1 else ""
|
|
151
|
+
|
|
152
|
+
return status
|
|
153
|
+
|
|
154
|
+
def get_diff(self, staged: bool = False) -> str:
|
|
155
|
+
"""Get the current diff."""
|
|
156
|
+
args = ["diff"]
|
|
157
|
+
if staged:
|
|
158
|
+
args.append("--staged")
|
|
159
|
+
_, output, _ = _run_git(args, self.cwd)
|
|
160
|
+
return output or "(no changes)"
|
|
161
|
+
|
|
162
|
+
def get_log(self, count: int = 10) -> str:
|
|
163
|
+
"""Get recent commit log."""
|
|
164
|
+
_, output, _ = _run_git(
|
|
165
|
+
["log", f"-{count}", "--oneline", "--decorate"],
|
|
166
|
+
self.cwd,
|
|
167
|
+
)
|
|
168
|
+
return output or "(no commits)"
|
|
169
|
+
|
|
170
|
+
# ── Branch ──────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def create_branch(self, name: str, switch: bool = True) -> tuple[bool, str]:
|
|
173
|
+
"""
|
|
174
|
+
Create a new feature branch from current HEAD.
|
|
175
|
+
Sanitizes the branch name.
|
|
176
|
+
"""
|
|
177
|
+
# Sanitize branch name
|
|
178
|
+
safe_name = re.sub(r"[^a-zA-Z0-9._/-]", "-", name.lower())
|
|
179
|
+
safe_name = re.sub(r"-+", "-", safe_name).strip("-")
|
|
180
|
+
if not safe_name:
|
|
181
|
+
safe_name = f"feature-{int(time.time())}"
|
|
182
|
+
|
|
183
|
+
code, out, err = _run_git(["checkout", "-b", safe_name], self.cwd)
|
|
184
|
+
if code == 0:
|
|
185
|
+
self._branches_created.append(safe_name)
|
|
186
|
+
return True, f"Branch created: {safe_name}"
|
|
187
|
+
return False, err or "Failed to create branch"
|
|
188
|
+
|
|
189
|
+
def switch_branch(self, name: str) -> tuple[bool, str]:
|
|
190
|
+
"""Switch to an existing branch."""
|
|
191
|
+
code, out, err = _run_git(["checkout", name], self.cwd)
|
|
192
|
+
return code == 0, out or err
|
|
193
|
+
|
|
194
|
+
def list_branches(self) -> str:
|
|
195
|
+
"""List local branches."""
|
|
196
|
+
_, out, _ = _run_git(["branch"], self.cwd)
|
|
197
|
+
return out or ""
|
|
198
|
+
|
|
199
|
+
# ── Commit ──────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def commit(self, message: str = "", files: list[str] | None = None,
|
|
202
|
+
all_changes: bool = True) -> tuple[bool, str]:
|
|
203
|
+
"""
|
|
204
|
+
Stage and commit changes with a meaningful message.
|
|
205
|
+
Auto-generates message if none provided.
|
|
206
|
+
"""
|
|
207
|
+
status = self.get_status()
|
|
208
|
+
if status.clean:
|
|
209
|
+
return False, "Nothing to commit (working tree clean)"
|
|
210
|
+
|
|
211
|
+
# Stage files
|
|
212
|
+
if all_changes:
|
|
213
|
+
code, _, err = _run_git(["add", "-A"], self.cwd)
|
|
214
|
+
elif files:
|
|
215
|
+
code, _, err = _run_git(["add"] + files, self.cwd)
|
|
216
|
+
else:
|
|
217
|
+
return False, "No files specified"
|
|
218
|
+
|
|
219
|
+
if code != 0:
|
|
220
|
+
return False, err
|
|
221
|
+
|
|
222
|
+
# Check for secrets in staged changes
|
|
223
|
+
secret_check = self._check_secrets()
|
|
224
|
+
if secret_check:
|
|
225
|
+
logger.warning("Potential secrets in commit: %s", secret_check)
|
|
226
|
+
|
|
227
|
+
# Generate commit message if not provided
|
|
228
|
+
if not message:
|
|
229
|
+
message = self._generate_commit_message()
|
|
230
|
+
|
|
231
|
+
# Check diff size
|
|
232
|
+
_, diff, _ = _run_git(["diff", "--staged", "--stat"], self.cwd)
|
|
233
|
+
line_count = diff.count("\n") if diff else 0
|
|
234
|
+
if line_count > 500:
|
|
235
|
+
logger.warning("Large commit: %d files changed", line_count)
|
|
236
|
+
|
|
237
|
+
code, _, err = _run_git(["commit", "-m", message], self.cwd)
|
|
238
|
+
if code == 0:
|
|
239
|
+
self._commits_made.append(message)
|
|
240
|
+
return True, f"Committed: {message}"
|
|
241
|
+
return False, err or "Commit failed"
|
|
242
|
+
|
|
243
|
+
def _generate_commit_message(self) -> str:
|
|
244
|
+
"""Generate a descriptive commit message from the diff."""
|
|
245
|
+
_, diff, _ = _run_git(["diff", "--staged", "--stat"], self.cwd)
|
|
246
|
+
|
|
247
|
+
if not diff:
|
|
248
|
+
return "Update files"
|
|
249
|
+
|
|
250
|
+
# Extract file patterns
|
|
251
|
+
files = []
|
|
252
|
+
for line in diff.split("\n"):
|
|
253
|
+
if "|" in line:
|
|
254
|
+
fname = line.split("|")[0].strip()
|
|
255
|
+
files.append(fname)
|
|
256
|
+
|
|
257
|
+
if not files:
|
|
258
|
+
return "Update files"
|
|
259
|
+
|
|
260
|
+
# All files in a diff are changes (new, modified, or deleted)
|
|
261
|
+
py_files = [f for f in files if f.endswith(".py")]
|
|
262
|
+
js_files = [f for f in files if f.endswith((".js", ".ts", ".jsx", ".tsx"))]
|
|
263
|
+
test_files = [f for f in files if "test" in f.lower()]
|
|
264
|
+
|
|
265
|
+
if test_files:
|
|
266
|
+
return f"test: add/update tests ({len(files)} files)"
|
|
267
|
+
|
|
268
|
+
if len(files) == 1:
|
|
269
|
+
return f"Update {os.path.basename(files[0])}"
|
|
270
|
+
|
|
271
|
+
# Group by directory
|
|
272
|
+
dirs = {}
|
|
273
|
+
for f in files:
|
|
274
|
+
d = os.path.dirname(f) or "root"
|
|
275
|
+
dirs.setdefault(d, []).append(f)
|
|
276
|
+
|
|
277
|
+
if len(dirs) == 1:
|
|
278
|
+
d = list(dirs.keys())[0]
|
|
279
|
+
return f"Update {d}/ ({len(files)} files)"
|
|
280
|
+
|
|
281
|
+
# Generic but descriptive
|
|
282
|
+
if py_files:
|
|
283
|
+
return f"refactor: update Python code ({len(files)} files)"
|
|
284
|
+
if js_files:
|
|
285
|
+
return f"feat: update frontend code ({len(files)} files)"
|
|
286
|
+
|
|
287
|
+
return f"chore: update {len(files)} files"
|
|
288
|
+
|
|
289
|
+
def _check_secrets(self) -> str | None:
|
|
290
|
+
"""Check staged changes for potential secrets."""
|
|
291
|
+
_, diff, _ = _run_git(["diff", "--staged"], self.cwd)
|
|
292
|
+
if not diff:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
# Patterns that look like secrets
|
|
296
|
+
secret_patterns = [
|
|
297
|
+
(r"(?:api_key|apikey|secret|password|token)\s*[:=]\s*[\"'`][^\s\"'`]{20,}[\"'`]", "API key / secret"),
|
|
298
|
+
(r"-----BEGIN (?:RSA |EC )?PRIVATE KEY-----", "Private key"),
|
|
299
|
+
(r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}", "GitHub token"),
|
|
300
|
+
(r"AKIA[0-9A-Z]{16}", "AWS access key"),
|
|
301
|
+
(r"sk-[A-Za-z0-9]{32,}", "OpenAI API key"),
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
for pattern, name in secret_patterns:
|
|
305
|
+
if re.search(pattern, diff, re.IGNORECASE):
|
|
306
|
+
return name
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
# ── Undo ────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
def undo_commit(self) -> tuple[bool, str]:
|
|
312
|
+
"""Undo the last commit (soft reset — keeps changes staged)."""
|
|
313
|
+
code, _, err = _run_git(["reset", "--soft", "HEAD~1"], self.cwd)
|
|
314
|
+
if code == 0:
|
|
315
|
+
if self._commits_made:
|
|
316
|
+
undone = self._commits_made.pop()
|
|
317
|
+
return True, f"Undid: {undone}"
|
|
318
|
+
return True, "Undid last commit (soft reset)"
|
|
319
|
+
return False, err or "Cannot undo — no previous commit?"
|
|
320
|
+
|
|
321
|
+
# ── Stash ────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def stash(self, message: str = "") -> tuple[bool, str]:
|
|
324
|
+
"""Stash current changes."""
|
|
325
|
+
args = ["stash", "push"]
|
|
326
|
+
if message:
|
|
327
|
+
args.extend(["-m", message])
|
|
328
|
+
code, out, err = _run_git(args, self.cwd)
|
|
329
|
+
return code == 0, out or err or "Stashed"
|
|
330
|
+
|
|
331
|
+
def stash_pop(self) -> tuple[bool, str]:
|
|
332
|
+
"""Pop the most recent stash."""
|
|
333
|
+
code, out, err = _run_git(["stash", "pop"], self.cwd)
|
|
334
|
+
return code == 0, out or err or "Popped stash"
|
|
335
|
+
|
|
336
|
+
# ── Safety checks ───────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
def pre_operation_check(self) -> tuple[bool, str]:
|
|
339
|
+
"""
|
|
340
|
+
Check if it's safe to perform git operations.
|
|
341
|
+
Returns (safe, reason).
|
|
342
|
+
"""
|
|
343
|
+
# Check for detached HEAD
|
|
344
|
+
_, branch, _ = _run_git(["branch", "--show-current"], self.cwd)
|
|
345
|
+
if not branch:
|
|
346
|
+
return False, "Detached HEAD — create or switch to a branch first."
|
|
347
|
+
|
|
348
|
+
# Check for rebase in progress
|
|
349
|
+
if (self.cwd / ".git" / "rebase-merge").exists():
|
|
350
|
+
return False, "Rebase in progress. Complete or abort it first."
|
|
351
|
+
if (self.cwd / ".git" / "rebase-apply").exists():
|
|
352
|
+
return False, "Rebase in progress. Complete or abort it first."
|
|
353
|
+
|
|
354
|
+
# Check for merge in progress
|
|
355
|
+
if (self.cwd / ".git" / "MERGE_HEAD").exists():
|
|
356
|
+
return False, "Merge in progress. Complete or abort it first."
|
|
357
|
+
|
|
358
|
+
return True, ""
|
|
359
|
+
|
|
360
|
+
# ── Session summary ─────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
def session_summary(self) -> str:
|
|
363
|
+
"""Summarize all git activity in this session."""
|
|
364
|
+
lines = []
|
|
365
|
+
if self._branches_created:
|
|
366
|
+
lines.append(f"Branches created: {', '.join(self._branches_created)}")
|
|
367
|
+
if self._commits_made:
|
|
368
|
+
lines.append(f"Commits: {len(self._commits_made)}")
|
|
369
|
+
for c in self._commits_made[-5:]:
|
|
370
|
+
lines.append(f" - {c[:80]}")
|
|
371
|
+
return "\n".join(lines) if lines else "(no git activity in this session)"
|