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,360 @@
1
+ """API router for merge and conflict resolution operations."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, status
7
+ from sqlalchemy import select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from sqlalchemy.orm import selectinload
10
+
11
+ from app.database import get_db
12
+ from app.exceptions import ConflictError, ResourceNotFoundError, ValidationError
13
+ from app.models.board import Board
14
+ from app.models.ticket import Ticket
15
+ from app.schemas.merge import (
16
+ AbortResponse,
17
+ ConflictStatusResponse,
18
+ MergeRequest,
19
+ MergeResponse,
20
+ MergeStatusResponse,
21
+ PushResponse,
22
+ PushStatusResponse,
23
+ RebaseResponse,
24
+ )
25
+ from app.services.merge_service import MergeService, MergeStrategy
26
+
27
+ router = APIRouter(prefix="/tickets", tags=["merge"])
28
+
29
+
30
+ async def _get_ticket_worktree(ticket_id: str, db: AsyncSession) -> tuple[Ticket, Path]:
31
+ """Get ticket and its active worktree path. Raises HTTP errors."""
32
+ result = await db.execute(
33
+ select(Ticket)
34
+ .where(Ticket.id == ticket_id)
35
+ .options(selectinload(Ticket.workspace))
36
+ )
37
+ ticket = result.scalar_one_or_none()
38
+ if not ticket:
39
+ raise HTTPException(status_code=404, detail=f"Ticket {ticket_id} not found")
40
+ if not ticket.workspace or not ticket.workspace.is_active:
41
+ raise HTTPException(status_code=404, detail="Ticket has no active worktree")
42
+
43
+ worktree_path = Path(ticket.workspace.worktree_path)
44
+ if not worktree_path.exists():
45
+ raise HTTPException(status_code=404, detail="Worktree directory not found")
46
+
47
+ return ticket, worktree_path
48
+
49
+
50
+ @router.post(
51
+ "/{ticket_id}/merge",
52
+ response_model=MergeResponse,
53
+ summary="Merge a ticket's changes into the default branch",
54
+ responses={
55
+ 200: {"description": "Merge completed (check success field)"},
56
+ 404: {"description": "Ticket not found"},
57
+ 409: {"description": "Merge conflict or validation error"},
58
+ 422: {"description": "Invalid request"},
59
+ },
60
+ )
61
+ async def merge_ticket(
62
+ ticket_id: str,
63
+ data: MergeRequest,
64
+ db: AsyncSession = Depends(get_db),
65
+ ) -> MergeResponse:
66
+ """
67
+ Merge a ticket's worktree branch into the default branch.
68
+
69
+ Prerequisites:
70
+ - Ticket must be in 'done' state
71
+ - Ticket must have an approved revision
72
+ - Ticket must have an active worktree
73
+
74
+ The merge will:
75
+ 1. Verify the worktree has no uncommitted changes
76
+ 2. Checkout the default branch in the main repo
77
+ 3. Pull latest changes (optional, based on config)
78
+ 4. Merge or rebase the worktree branch
79
+ 5. Optionally delete the worktree and cleanup artifacts
80
+
81
+ If the merge fails due to conflicts, the merge is aborted and the
82
+ worktree is left intact for manual resolution.
83
+ """
84
+ # Load board config for merge settings
85
+ board_config = None
86
+ ticket_result = await db.execute(select(Ticket).where(Ticket.id == ticket_id))
87
+ ticket_obj = ticket_result.scalar_one_or_none()
88
+ if ticket_obj and ticket_obj.board_id:
89
+ board_result = await db.execute(
90
+ select(Board).where(Board.id == ticket_obj.board_id)
91
+ )
92
+ board_obj = board_result.scalar_one_or_none()
93
+ if board_obj:
94
+ board_config = board_obj.config
95
+
96
+ service = MergeService(db, board_config=board_config)
97
+
98
+ try:
99
+ # Convert schema enum to service enum
100
+ strategy = (
101
+ MergeStrategy.MERGE
102
+ if data.strategy.value == "merge"
103
+ else MergeStrategy.REBASE
104
+ )
105
+
106
+ result = await service.merge_ticket(
107
+ ticket_id=ticket_id,
108
+ strategy=strategy,
109
+ delete_worktree=data.delete_worktree,
110
+ cleanup_artifacts=data.cleanup_artifacts,
111
+ )
112
+
113
+ return MergeResponse(
114
+ success=result.success,
115
+ message=result.message,
116
+ exit_code=result.exit_code,
117
+ evidence_id=result.evidence_ids.get("meta_id")
118
+ if result.evidence_ids
119
+ else None,
120
+ pull_warning=result.pull_warning,
121
+ )
122
+
123
+ except ResourceNotFoundError as e:
124
+ raise HTTPException(
125
+ status_code=status.HTTP_404_NOT_FOUND,
126
+ detail=str(e),
127
+ ) from e
128
+ except ValidationError as e:
129
+ raise HTTPException(
130
+ status_code=status.HTTP_409_CONFLICT,
131
+ detail=str(e),
132
+ ) from e
133
+ except ConflictError as e:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_409_CONFLICT,
136
+ detail=str(e),
137
+ ) from e
138
+
139
+
140
+ @router.get(
141
+ "/{ticket_id}/merge-status",
142
+ response_model=MergeStatusResponse,
143
+ summary="Get merge status for a ticket",
144
+ )
145
+ async def get_merge_status(
146
+ ticket_id: str,
147
+ db: AsyncSession = Depends(get_db),
148
+ ) -> MergeStatusResponse:
149
+ """
150
+ Get the merge status for a ticket.
151
+
152
+ Returns whether the ticket can be merged, whether it has already been
153
+ merged, and information about the workspace and last merge attempt.
154
+ """
155
+ service = MergeService(db)
156
+
157
+ try:
158
+ status_info = await service.get_merge_status(ticket_id)
159
+ return MergeStatusResponse(**status_info)
160
+ except ResourceNotFoundError as e:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_404_NOT_FOUND,
163
+ detail=str(e),
164
+ ) from e
165
+
166
+
167
+ # ===================== Conflict Resolution Endpoints =====================
168
+
169
+
170
+ @router.get(
171
+ "/{ticket_id}/conflict-status",
172
+ response_model=ConflictStatusResponse,
173
+ summary="Check if a ticket's worktree has conflicts",
174
+ )
175
+ async def get_conflict_status(
176
+ ticket_id: str,
177
+ db: AsyncSession = Depends(get_db),
178
+ ) -> ConflictStatusResponse:
179
+ """Check for conflicts in the ticket's worktree.
180
+
181
+ Returns conflict state, affected files, and whether continue/abort is possible.
182
+ Also returns branch divergence info for merge planning.
183
+ """
184
+ from app.services.git_ops import detect_conflict_state, get_divergence_info
185
+
186
+ ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
187
+
188
+ state = await asyncio.to_thread(detect_conflict_state, worktree_path)
189
+
190
+ # Get divergence info
191
+ from app.services.workspace_service import WorkspaceService
192
+
193
+ repo_path = WorkspaceService.get_repo_path()
194
+ branch_name = ticket.workspace.branch_name
195
+ divergence = await asyncio.to_thread(get_divergence_info, repo_path, branch_name)
196
+
197
+ if state is None:
198
+ return ConflictStatusResponse(
199
+ has_conflict=False,
200
+ operation=None,
201
+ conflicted_files=[],
202
+ can_continue=False,
203
+ can_abort=False,
204
+ divergence=divergence,
205
+ )
206
+
207
+ return ConflictStatusResponse(
208
+ has_conflict=True,
209
+ operation=state.operation,
210
+ conflicted_files=state.conflicted_files,
211
+ can_continue=state.can_continue,
212
+ can_abort=state.can_abort,
213
+ divergence=divergence,
214
+ )
215
+
216
+
217
+ @router.post(
218
+ "/{ticket_id}/rebase",
219
+ response_model=RebaseResponse,
220
+ summary="Rebase a ticket's branch onto the target branch",
221
+ )
222
+ async def rebase_ticket(
223
+ ticket_id: str,
224
+ onto_branch: str = "main",
225
+ db: AsyncSession = Depends(get_db),
226
+ ) -> RebaseResponse:
227
+ """Rebase the ticket's worktree branch onto the target branch.
228
+
229
+ Use this when the base branch has moved forward (divergence detected).
230
+ If conflicts arise, use continue-rebase or abort-conflict.
231
+ """
232
+ from app.services.git_ops import rebase_branch
233
+
234
+ _, worktree_path = await _get_ticket_worktree(ticket_id, db)
235
+
236
+ result = await asyncio.to_thread(rebase_branch, worktree_path, onto_branch)
237
+
238
+ return RebaseResponse(
239
+ success=result.success,
240
+ message=result.message,
241
+ has_conflicts=result.has_conflicts,
242
+ conflicted_files=result.conflicted_files,
243
+ )
244
+
245
+
246
+ @router.post(
247
+ "/{ticket_id}/continue-rebase",
248
+ response_model=RebaseResponse,
249
+ summary="Continue a paused rebase after resolving conflicts",
250
+ )
251
+ async def continue_rebase_endpoint(
252
+ ticket_id: str,
253
+ db: AsyncSession = Depends(get_db),
254
+ ) -> RebaseResponse:
255
+ """Continue a rebase that paused due to conflicts.
256
+
257
+ Call this after the AI agent (or user) has resolved conflicts in the worktree.
258
+ """
259
+ from app.services.git_ops import continue_rebase
260
+
261
+ _, worktree_path = await _get_ticket_worktree(ticket_id, db)
262
+
263
+ result = await asyncio.to_thread(continue_rebase, worktree_path)
264
+
265
+ return RebaseResponse(
266
+ success=result.success,
267
+ message=result.message,
268
+ has_conflicts=result.has_conflicts,
269
+ conflicted_files=result.conflicted_files,
270
+ )
271
+
272
+
273
+ @router.post(
274
+ "/{ticket_id}/abort-conflict",
275
+ response_model=AbortResponse,
276
+ summary="Abort the current conflict operation",
277
+ )
278
+ async def abort_conflict_endpoint(
279
+ ticket_id: str,
280
+ db: AsyncSession = Depends(get_db),
281
+ ) -> AbortResponse:
282
+ """Abort the current conflict operation (rebase, merge, cherry-pick, etc.).
283
+
284
+ Returns the worktree to its pre-operation state.
285
+ """
286
+ from app.services.git_ops import abort_operation
287
+
288
+ _, worktree_path = await _get_ticket_worktree(ticket_id, db)
289
+
290
+ success = await asyncio.to_thread(abort_operation, worktree_path)
291
+
292
+ return AbortResponse(
293
+ success=success,
294
+ message="Operation aborted successfully"
295
+ if success
296
+ else "Failed to abort operation",
297
+ )
298
+
299
+
300
+ # ===================== Push Endpoints =====================
301
+
302
+
303
+ @router.get(
304
+ "/{ticket_id}/push-status",
305
+ response_model=PushStatusResponse,
306
+ summary="Check if a ticket's branch needs to be pushed",
307
+ )
308
+ async def get_push_status_endpoint(
309
+ ticket_id: str,
310
+ db: AsyncSession = Depends(get_db),
311
+ ) -> PushStatusResponse:
312
+ """Check if the ticket's branch is ahead/behind the remote tracking branch."""
313
+ from app.services.git_ops import get_push_status
314
+
315
+ ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
316
+ branch_name = ticket.workspace.branch_name
317
+
318
+ result = await asyncio.to_thread(get_push_status, worktree_path, branch_name)
319
+ return PushStatusResponse(**result)
320
+
321
+
322
+ @router.post(
323
+ "/{ticket_id}/push",
324
+ response_model=PushResponse,
325
+ summary="Push a ticket's branch to remote",
326
+ )
327
+ async def push_ticket_branch(
328
+ ticket_id: str,
329
+ db: AsyncSession = Depends(get_db),
330
+ ) -> PushResponse:
331
+ """Push the ticket's worktree branch to the remote origin."""
332
+ from app.services.git_ops import push_branch
333
+
334
+ ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
335
+ branch_name = ticket.workspace.branch_name
336
+
337
+ result = await asyncio.to_thread(push_branch, worktree_path, branch_name)
338
+ return PushResponse(success=result.success, message=result.message)
339
+
340
+
341
+ @router.post(
342
+ "/{ticket_id}/force-push",
343
+ response_model=PushResponse,
344
+ summary="Force-push a ticket's branch to remote (with lease)",
345
+ )
346
+ async def force_push_ticket_branch(
347
+ ticket_id: str,
348
+ db: AsyncSession = Depends(get_db),
349
+ ) -> PushResponse:
350
+ """Force-push the ticket's branch using --force-with-lease for safety.
351
+
352
+ Use after rebase when the remote branch already exists.
353
+ """
354
+ from app.services.git_ops import force_push_branch
355
+
356
+ ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
357
+ branch_name = ticket.workspace.branch_name
358
+
359
+ result = await asyncio.to_thread(force_push_branch, worktree_path, branch_name)
360
+ return PushResponse(success=result.success, message=result.message)