draft-board 0.1.0-beta.0

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 (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. package/package.json +37 -0
@@ -0,0 +1,346 @@
1
+ """Simple git merge operations without state machine coupling.
2
+
3
+ This module provides straightforward git merge operations that can be called
4
+ from any context without requiring specific ticket states or validation.
5
+ """
6
+
7
+ import logging
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class SimpleMergeResult:
17
+ """Result of a simple git merge operation."""
18
+
19
+ success: bool
20
+ message: str
21
+ merged_branch: str | None = None
22
+ target_branch: str | None = None
23
+ merge_commit: str | None = None
24
+
25
+
26
+ class GitMergeError(Exception):
27
+ """Raised when git merge operations fail."""
28
+
29
+ pass
30
+
31
+
32
+ def git_merge_worktree_branch(
33
+ repo_path: Path,
34
+ branch_name: str,
35
+ target_branch: str = "main",
36
+ delete_branch_after: bool = True,
37
+ push_to_remote: bool = False,
38
+ squash: bool = False,
39
+ check_divergence: bool = True,
40
+ ) -> SimpleMergeResult:
41
+ """Merge a worktree branch into the target branch.
42
+
43
+ This is a simple, synchronous git merge operation with no state validation
44
+ or database coupling. It just runs git commands.
45
+
46
+ Args:
47
+ repo_path: Path to the main git repository (not the worktree)
48
+ branch_name: Name of the branch to merge (e.g., "goal/xxx/ticket/yyy")
49
+ target_branch: Target branch to merge into (default: "main")
50
+ delete_branch_after: Whether to delete the branch after merge
51
+ push_to_remote: Whether to push to remote after merge
52
+ squash: Whether to squash commits (single commit per task)
53
+ check_divergence: Whether to check if base branch moved ahead
54
+
55
+ Returns:
56
+ SimpleMergeResult with success status and details
57
+
58
+ Raises:
59
+ GitMergeError: If git operations fail
60
+ """
61
+ logger.info(
62
+ f"Starting simple merge: {branch_name} -> {target_branch} (squash={squash})"
63
+ )
64
+
65
+ try:
66
+ # 1. Ensure we're in the repo (not a worktree)
67
+ if not (repo_path / ".git").exists():
68
+ raise GitMergeError(f"Not a git repository: {repo_path}")
69
+
70
+ # 2. Fetch latest (if remote exists)
71
+ logger.info("Fetching latest from remote...")
72
+ result = subprocess.run(
73
+ ["git", "fetch", "origin"],
74
+ cwd=repo_path,
75
+ capture_output=True,
76
+ text=True,
77
+ timeout=30,
78
+ )
79
+ if result.returncode != 0:
80
+ logger.warning(f"Git fetch failed (may not have remote): {result.stderr}")
81
+
82
+ # 3. Divergence check (copied from Vibe Kanban!)
83
+ if check_divergence:
84
+ logger.info("Checking for divergence...")
85
+ # Count commits in target_branch that are not in branch_name
86
+ result = subprocess.run(
87
+ ["git", "rev-list", "--count", f"{branch_name}..{target_branch}"],
88
+ cwd=repo_path,
89
+ capture_output=True,
90
+ text=True,
91
+ timeout=10,
92
+ )
93
+ if result.returncode == 0:
94
+ commits_behind = int(result.stdout.strip())
95
+ if commits_behind > 0:
96
+ raise GitMergeError(
97
+ f"Cannot merge: {target_branch} is {commits_behind} commits ahead of {branch_name}. "
98
+ f"The base branch has moved forward since the task was created. "
99
+ f"Rebase the task branch onto {target_branch} first."
100
+ )
101
+ logger.info("✓ No divergence detected")
102
+ else:
103
+ logger.warning(f"Divergence check failed: {result.stderr}")
104
+
105
+ # 4. Checkout target branch
106
+ logger.info(f"Checking out {target_branch}...")
107
+ result = subprocess.run(
108
+ ["git", "checkout", target_branch],
109
+ cwd=repo_path,
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=10,
113
+ )
114
+ if result.returncode != 0:
115
+ raise GitMergeError(f"Failed to checkout {target_branch}: {result.stderr}")
116
+
117
+ # 4. Pull latest from target branch
118
+ logger.info(f"Pulling latest {target_branch}...")
119
+ result = subprocess.run(
120
+ ["git", "pull", "origin", target_branch],
121
+ cwd=repo_path,
122
+ capture_output=True,
123
+ text=True,
124
+ timeout=30,
125
+ )
126
+ if result.returncode != 0:
127
+ logger.warning(f"Git pull failed (may not have remote): {result.stderr}")
128
+
129
+ # 5. Merge the branch (squash or regular)
130
+ if squash:
131
+ # Squash merge - all commits become one (like Vibe Kanban!)
132
+ logger.info(f"Squash merging {branch_name} into {target_branch}...")
133
+
134
+ # Check if branches have diverged (are there actual changes?)
135
+ diff_check = subprocess.run(
136
+ ["git", "diff", f"{target_branch}...{branch_name}", "--stat"],
137
+ cwd=repo_path,
138
+ capture_output=True,
139
+ text=True,
140
+ timeout=10,
141
+ )
142
+
143
+ if not diff_check.stdout.strip():
144
+ logger.info(
145
+ f"No changes between {target_branch} and {branch_name} - branches are identical"
146
+ )
147
+ # No actual changes, but this is considered success
148
+ # Skip the merge since branches are already in sync
149
+ merge_commit = None # No new commit created
150
+ else:
151
+ logger.info(f"Changes detected:\n{diff_check.stdout}")
152
+
153
+ # Stage all changes from branch
154
+ result = subprocess.run(
155
+ ["git", "merge", "--squash", branch_name],
156
+ cwd=repo_path,
157
+ capture_output=True,
158
+ text=True,
159
+ timeout=30,
160
+ )
161
+ if result.returncode != 0:
162
+ if "CONFLICT" in result.stdout or "CONFLICT" in result.stderr:
163
+ raise GitMergeError(
164
+ f"Merge conflict during squash: {result.stderr}"
165
+ )
166
+ raise GitMergeError(f"Squash merge failed: {result.stderr}")
167
+
168
+ # Check if there are changes to commit
169
+ status_result = subprocess.run(
170
+ ["git", "status", "--porcelain"],
171
+ cwd=repo_path,
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=5,
175
+ )
176
+
177
+ if not status_result.stdout.strip():
178
+ # No changes to commit - this shouldn't happen after squash
179
+ logger.warning(
180
+ "No changes staged after squash merge - unexpected state"
181
+ )
182
+ else:
183
+ # Create single commit
184
+ result = subprocess.run(
185
+ [
186
+ "git",
187
+ "commit",
188
+ "-m",
189
+ f"Merge {branch_name} into {target_branch}",
190
+ ],
191
+ cwd=repo_path,
192
+ capture_output=True,
193
+ text=True,
194
+ timeout=10,
195
+ )
196
+ if result.returncode != 0:
197
+ # Log both stdout and stderr for debugging
198
+ logger.error(
199
+ f"Git commit failed. Stdout: {result.stdout}, Stderr: {result.stderr}"
200
+ )
201
+ raise GitMergeError(
202
+ f"Commit after squash failed: {result.stderr}\n"
203
+ f"Stdout: {result.stdout}\n"
204
+ f"This might be due to git user config not being set."
205
+ )
206
+
207
+ logger.info("Squash merge successful")
208
+ else:
209
+ # Regular merge with --no-ff
210
+ logger.info(f"Merging {branch_name} into {target_branch}...")
211
+ result = subprocess.run(
212
+ [
213
+ "git",
214
+ "merge",
215
+ "--no-ff",
216
+ branch_name,
217
+ "-m",
218
+ f"Merge {branch_name} into {target_branch}",
219
+ ],
220
+ cwd=repo_path,
221
+ capture_output=True,
222
+ text=True,
223
+ timeout=30,
224
+ )
225
+ if result.returncode != 0:
226
+ # Check if it's a conflict
227
+ if "CONFLICT" in result.stdout or "CONFLICT" in result.stderr:
228
+ raise GitMergeError(f"Merge conflict: {result.stderr}")
229
+ raise GitMergeError(f"Merge failed: {result.stderr}")
230
+
231
+ logger.info("Merge successful")
232
+
233
+ # 6. Get merge commit hash (only if we didn't already set it)
234
+ if "merge_commit" not in locals():
235
+ result = subprocess.run(
236
+ ["git", "rev-parse", "HEAD"],
237
+ cwd=repo_path,
238
+ capture_output=True,
239
+ text=True,
240
+ timeout=5,
241
+ )
242
+ merge_commit = result.stdout.strip() if result.returncode == 0 else None
243
+
244
+ # 7. Push to remote (if requested)
245
+ if push_to_remote:
246
+ logger.info(f"Pushing {target_branch} to remote...")
247
+ result = subprocess.run(
248
+ ["git", "push", "origin", target_branch],
249
+ cwd=repo_path,
250
+ capture_output=True,
251
+ text=True,
252
+ timeout=30,
253
+ )
254
+ if result.returncode != 0:
255
+ logger.warning(f"Push failed: {result.stderr}")
256
+ # Don't fail the merge if push fails
257
+
258
+ # 8. Delete branch (if requested)
259
+ if delete_branch_after:
260
+ logger.info(f"Deleting branch {branch_name}...")
261
+ result = subprocess.run(
262
+ ["git", "branch", "-d", branch_name],
263
+ cwd=repo_path,
264
+ capture_output=True,
265
+ text=True,
266
+ timeout=5,
267
+ )
268
+ if result.returncode != 0:
269
+ # Try force delete
270
+ result = subprocess.run(
271
+ ["git", "branch", "-D", branch_name],
272
+ cwd=repo_path,
273
+ capture_output=True,
274
+ text=True,
275
+ timeout=5,
276
+ )
277
+ if result.returncode != 0:
278
+ logger.warning(f"Failed to delete branch: {result.stderr}")
279
+
280
+ # Prepare success message based on whether changes were merged
281
+ if merge_commit:
282
+ message = f"Successfully merged {branch_name} into {target_branch}"
283
+ else:
284
+ message = f"Branches {branch_name} and {target_branch} are already in sync (no changes to merge)"
285
+
286
+ return SimpleMergeResult(
287
+ success=True,
288
+ message=message,
289
+ merged_branch=branch_name,
290
+ target_branch=target_branch,
291
+ merge_commit=merge_commit,
292
+ )
293
+
294
+ except subprocess.TimeoutExpired as e:
295
+ raise GitMergeError(f"Git operation timed out: {e}")
296
+ except Exception as e:
297
+ logger.error(f"Merge failed: {e}")
298
+ raise GitMergeError(str(e))
299
+
300
+
301
+ def cleanup_worktree(repo_path: Path, worktree_path: Path) -> bool:
302
+ """Remove a git worktree directory.
303
+
304
+ Args:
305
+ repo_path: Path to the main git repository
306
+ worktree_path: Path to the worktree to remove
307
+
308
+ Returns:
309
+ True if cleanup succeeded, False otherwise
310
+ """
311
+ logger.info(f"Cleaning up worktree: {worktree_path}")
312
+
313
+ try:
314
+ # Remove worktree using git
315
+ result = subprocess.run(
316
+ ["git", "worktree", "remove", "--force", str(worktree_path)],
317
+ cwd=repo_path,
318
+ capture_output=True,
319
+ text=True,
320
+ timeout=10,
321
+ )
322
+
323
+ if result.returncode != 0:
324
+ logger.warning(f"Git worktree remove failed: {result.stderr}")
325
+ # Try to remove directory manually
326
+ import shutil
327
+
328
+ if worktree_path.exists():
329
+ shutil.rmtree(worktree_path)
330
+ logger.info("Manually removed worktree directory")
331
+
332
+ # Prune stale worktree references
333
+ subprocess.run(
334
+ ["git", "worktree", "prune"],
335
+ cwd=repo_path,
336
+ capture_output=True,
337
+ text=True,
338
+ timeout=5,
339
+ )
340
+
341
+ logger.info("Worktree cleanup complete")
342
+ return True
343
+
344
+ except Exception as e:
345
+ logger.error(f"Worktree cleanup failed: {e}")
346
+ return False