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,174 @@
1
+ """add_repos_and_board_repos
2
+
3
+ Revision ID: add_repos_001
4
+ Revises: 357c780ee445
5
+ Create Date: 2026-01-29 22:00:00.000000
6
+
7
+ """
8
+
9
+ import uuid
10
+ from collections.abc import Sequence
11
+ from pathlib import Path
12
+
13
+ import sqlalchemy as sa
14
+ from sqlalchemy.sql import column, table
15
+
16
+ from alembic import op
17
+
18
+ # revision identifiers, used by Alembic.
19
+ revision: str = "add_repos_001"
20
+ down_revision: str | None = "357c780ee445"
21
+ branch_labels: str | Sequence[str] | None = None
22
+ depends_on: str | Sequence[str] | None = None
23
+
24
+
25
+ def upgrade() -> None:
26
+ # Create repos table
27
+ op.create_table(
28
+ "repos",
29
+ sa.Column("id", sa.String(length=36), nullable=False),
30
+ sa.Column("path", sa.String(length=1024), nullable=False),
31
+ sa.Column("name", sa.String(length=255), nullable=False),
32
+ sa.Column("display_name", sa.String(length=255), nullable=False),
33
+ sa.Column("setup_script", sa.Text(), nullable=True),
34
+ sa.Column("cleanup_script", sa.Text(), nullable=True),
35
+ sa.Column("dev_server_script", sa.Text(), nullable=True),
36
+ sa.Column("default_branch", sa.String(length=255), nullable=True),
37
+ sa.Column("remote_url", sa.String(length=1024), nullable=True),
38
+ sa.Column(
39
+ "created_at",
40
+ sa.DateTime(),
41
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
42
+ nullable=False,
43
+ ),
44
+ sa.Column(
45
+ "updated_at",
46
+ sa.DateTime(),
47
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
48
+ nullable=False,
49
+ ),
50
+ sa.PrimaryKeyConstraint("id"),
51
+ sa.UniqueConstraint("path", name="uq_repo_path"),
52
+ )
53
+ op.create_index(op.f("ix_repos_path"), "repos", ["path"], unique=True)
54
+
55
+ # Create board_repos junction table
56
+ op.create_table(
57
+ "board_repos",
58
+ sa.Column("id", sa.String(length=36), nullable=False),
59
+ sa.Column("board_id", sa.String(length=36), nullable=False),
60
+ sa.Column("repo_id", sa.String(length=36), nullable=False),
61
+ sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="0"),
62
+ sa.Column("custom_setup_script", sa.Text(), nullable=True),
63
+ sa.Column("custom_cleanup_script", sa.Text(), nullable=True),
64
+ sa.Column(
65
+ "created_at",
66
+ sa.DateTime(),
67
+ server_default=sa.text("(CURRENT_TIMESTAMP)"),
68
+ nullable=False,
69
+ ),
70
+ sa.ForeignKeyConstraint(["board_id"], ["boards.id"], ondelete="CASCADE"),
71
+ sa.ForeignKeyConstraint(["repo_id"], ["repos.id"], ondelete="CASCADE"),
72
+ sa.PrimaryKeyConstraint("id"),
73
+ sa.UniqueConstraint("board_id", "repo_id", name="uq_board_repo"),
74
+ )
75
+ op.create_index(
76
+ op.f("ix_board_repos_board_id"), "board_repos", ["board_id"], unique=False
77
+ )
78
+ op.create_index(
79
+ op.f("ix_board_repos_repo_id"), "board_repos", ["repo_id"], unique=False
80
+ )
81
+
82
+ # Migrate existing boards.repo_root to new structure
83
+ # Note: This is a data migration, so we need to use SQLAlchemy core
84
+
85
+ # Define table structures for data migration
86
+ boards = table(
87
+ "boards",
88
+ column("id", sa.String),
89
+ column("repo_root", sa.String),
90
+ column("default_branch", sa.String),
91
+ )
92
+
93
+ repos_table = table(
94
+ "repos",
95
+ column("id", sa.String),
96
+ column("path", sa.String),
97
+ column("name", sa.String),
98
+ column("display_name", sa.String),
99
+ column("default_branch", sa.String),
100
+ )
101
+
102
+ board_repos_table = table(
103
+ "board_repos",
104
+ column("id", sa.String),
105
+ column("board_id", sa.String),
106
+ column("repo_id", sa.String),
107
+ column("is_primary", sa.Boolean),
108
+ )
109
+
110
+ # Get connection
111
+ conn = op.get_bind()
112
+
113
+ # Fetch all boards with repo_root
114
+ result = conn.execute(
115
+ sa.select(boards.c.id, boards.c.repo_root, boards.c.default_branch)
116
+ )
117
+ board_data = result.fetchall()
118
+
119
+ # For each board, create a repo and link it
120
+ for board_id, repo_root, default_branch in board_data:
121
+ if not repo_root:
122
+ continue
123
+
124
+ # Generate repo ID
125
+ repo_id = str(uuid.uuid4())
126
+
127
+ # Extract repo name from path
128
+ try:
129
+ repo_name = Path(repo_root).name
130
+ except Exception:
131
+ repo_name = "repository"
132
+
133
+ # Insert repo
134
+ conn.execute(
135
+ repos_table.insert().values(
136
+ id=repo_id,
137
+ path=repo_root,
138
+ name=repo_name,
139
+ display_name=repo_name,
140
+ default_branch=default_branch,
141
+ )
142
+ )
143
+
144
+ # Link board to repo (as primary)
145
+ board_repo_id = str(uuid.uuid4())
146
+ conn.execute(
147
+ board_repos_table.insert().values(
148
+ id=board_repo_id, board_id=board_id, repo_id=repo_id, is_primary=True
149
+ )
150
+ )
151
+
152
+ # Make repo_root nullable (backwards compatibility - keep for now)
153
+ # In a future migration, we can remove it entirely
154
+ with op.batch_alter_table("boards", schema=None) as batch_op:
155
+ batch_op.alter_column(
156
+ "repo_root", existing_type=sa.String(length=1024), nullable=True
157
+ )
158
+
159
+
160
+ def downgrade() -> None:
161
+ # Drop board_repos table
162
+ op.drop_index(op.f("ix_board_repos_repo_id"), table_name="board_repos")
163
+ op.drop_index(op.f("ix_board_repos_board_id"), table_name="board_repos")
164
+ op.drop_table("board_repos")
165
+
166
+ # Drop repos table
167
+ op.drop_index(op.f("ix_repos_path"), table_name="repos")
168
+ op.drop_table("repos")
169
+
170
+ # Restore repo_root to non-nullable
171
+ with op.batch_alter_table("boards", schema=None) as batch_op:
172
+ batch_op.alter_column(
173
+ "repo_root", existing_type=sa.String(length=1024), nullable=False
174
+ )
@@ -0,0 +1,27 @@
1
+ """Add session_id column to jobs table for executor session resume.
2
+
3
+ Revision ID: a1b2c3d4e5f6
4
+ Revises: 7b307e847cbd
5
+ Create Date: 2026-02-16 15:00:00.000000
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "a1b2c3d4e5f6"
17
+ down_revision: str | None = "7b307e847cbd"
18
+ branch_labels: str | Sequence[str] | None = None
19
+ depends_on: str | Sequence[str] | None = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ op.add_column("jobs", sa.Column("session_id", sa.String(255), nullable=True))
24
+
25
+
26
+ def downgrade() -> None:
27
+ op.drop_column("jobs", "session_id")
@@ -0,0 +1,104 @@
1
+ """add_sqlite_backend_tables
2
+
3
+ Revision ID: c3a8f1b2d4e5
4
+ Revises: b10fb0b62240
5
+ Create Date: 2026-02-15 10:00:00.000000
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "c3a8f1b2d4e5"
17
+ down_revision: str | None = "b10fb0b62240"
18
+ branch_labels: str | Sequence[str] | None = None
19
+ depends_on: str | Sequence[str] | None = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # Job queue (replaces Celery broker)
24
+ op.create_table(
25
+ "job_queue",
26
+ sa.Column("id", sa.String(36), primary_key=True),
27
+ sa.Column("task_name", sa.String(255), nullable=False),
28
+ sa.Column("args_json", sa.Text(), nullable=False, server_default="[]"),
29
+ sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
30
+ sa.Column("claimed_by", sa.String(255), nullable=True),
31
+ sa.Column("claimed_at", sa.DateTime(), nullable=True),
32
+ sa.Column("completed_at", sa.DateTime(), nullable=True),
33
+ sa.Column("result_json", sa.Text(), nullable=True),
34
+ sa.Column("priority", sa.Integer(), nullable=False, server_default="0"),
35
+ sa.Column(
36
+ "created_at",
37
+ sa.DateTime(),
38
+ nullable=False,
39
+ server_default=sa.func.now(),
40
+ ),
41
+ )
42
+ op.create_index("ix_job_queue_status", "job_queue", ["status"])
43
+ op.create_index(
44
+ "ix_job_queue_claim_order",
45
+ "job_queue",
46
+ ["status", "priority", "created_at"],
47
+ )
48
+
49
+ # Idempotency cache (replaces Redis SETNX + result cache)
50
+ op.create_table(
51
+ "idempotency_cache",
52
+ sa.Column("cache_key", sa.String(512), primary_key=True),
53
+ sa.Column("lock_value", sa.Text(), nullable=True),
54
+ sa.Column("result_value", sa.Text(), nullable=True),
55
+ sa.Column("lock_expires_at", sa.DateTime(), nullable=True),
56
+ sa.Column("result_expires_at", sa.DateTime(), nullable=True),
57
+ sa.Column(
58
+ "created_at",
59
+ sa.DateTime(),
60
+ nullable=False,
61
+ server_default=sa.func.now(),
62
+ ),
63
+ )
64
+
65
+ # Rate limit entries (replaces Redis sorted sets)
66
+ op.create_table(
67
+ "rate_limit_entries",
68
+ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
69
+ sa.Column("client_key", sa.String(255), nullable=False),
70
+ sa.Column("cost", sa.Integer(), nullable=False, server_default="1"),
71
+ sa.Column("recorded_at", sa.Float(), nullable=False),
72
+ sa.Column("expires_at", sa.Float(), nullable=False),
73
+ )
74
+ op.create_index(
75
+ "ix_rate_limit_entries_client_key",
76
+ "rate_limit_entries",
77
+ ["client_key"],
78
+ )
79
+ op.create_index(
80
+ "ix_rate_limit_entries_expires_at",
81
+ "rate_limit_entries",
82
+ ["expires_at"],
83
+ )
84
+
85
+ # KV store (replaces Redis GET/SET for queued messages)
86
+ op.create_table(
87
+ "kv_store",
88
+ sa.Column("key", sa.String(512), primary_key=True),
89
+ sa.Column("value", sa.Text(), nullable=False),
90
+ sa.Column("expires_at", sa.DateTime(), nullable=True),
91
+ sa.Column(
92
+ "created_at",
93
+ sa.DateTime(),
94
+ nullable=False,
95
+ server_default=sa.func.now(),
96
+ ),
97
+ )
98
+
99
+
100
+ def downgrade() -> None:
101
+ op.drop_table("kv_store")
102
+ op.drop_table("rate_limit_entries")
103
+ op.drop_table("idempotency_cache")
104
+ op.drop_table("job_queue")
@@ -0,0 +1,34 @@
1
+ """add_diff_content_to_revisions
2
+
3
+ Revision ID: b10fb0b62240
4
+ Revises: 9d17f0698d3b
5
+ Create Date: 2026-02-10 07:08:55.015460
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ import sqlalchemy as sa
12
+
13
+ from alembic import op
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "b10fb0b62240"
17
+ down_revision: str | None = "9d17f0698d3b"
18
+ branch_labels: str | Sequence[str] | None = None
19
+ depends_on: str | Sequence[str] | None = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # Add diff content columns to store diffs permanently in DB
24
+ # This preserves diffs even after worktree/evidence cleanup
25
+ op.add_column("revisions", sa.Column("diff_stat_content", sa.Text(), nullable=True))
26
+ op.add_column(
27
+ "revisions", sa.Column("diff_patch_content", sa.Text(), nullable=True)
28
+ )
29
+
30
+
31
+ def downgrade() -> None:
32
+ # Remove diff content columns
33
+ op.drop_column("revisions", "diff_patch_content")
34
+ op.drop_column("revisions", "diff_stat_content")
@@ -0,0 +1,89 @@
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # file_template = %%(rev)s_%%(slug)s
9
+
10
+ # sys.path path, will be prepended to sys.path if present.
11
+ # defaults to the current working directory.
12
+ prepend_sys_path = .
13
+
14
+ # version path separator; As mentioned above, this is around a ';' character
15
+ # by default, but may be changed with the '--sep' flag to 'alembic init'.
16
+ # version_path_separator = os
17
+
18
+ # set to 'true' to run the environment during
19
+ # the 'revision' command, regardless of autogenerate
20
+ # revision_environment = false
21
+
22
+ # set to 'true' to allow .pyc and .pyo files without
23
+ # a source .py file to be detected as revisions in the
24
+ # versions/ directory
25
+ # sourceless = false
26
+
27
+ # version location specification; This defaults
28
+ # to alembic/versions. When using multiple version
29
+ # directories, initial revisions must be specified with --version-path.
30
+ # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
31
+
32
+ # set to 'true' to search source files recursively
33
+ # in each "version_locations" directory
34
+ # recursive_version_locations = false
35
+
36
+ # the output encoding used when revision files
37
+ # are written from script.py.mako
38
+ # output_encoding = utf-8
39
+
40
+ sqlalchemy.url = sqlite:///./kanban.db
41
+
42
+
43
+ [post_write_hooks]
44
+ # post_write_hooks defines scripts or Python functions that are run
45
+ # on newly generated revision scripts. See the documentation for further
46
+ # detail and examples
47
+
48
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
49
+ # hooks = black
50
+ # black.type = console_scripts
51
+ # black.entrypoint = black
52
+ # black.options = -q
53
+
54
+ # Logging configuration
55
+ [loggers]
56
+ keys = root,sqlalchemy,alembic
57
+
58
+ [handlers]
59
+ keys = console
60
+
61
+ [formatters]
62
+ keys = generic
63
+
64
+ [logger_root]
65
+ level = WARN
66
+ handlers = console
67
+ qualname =
68
+
69
+ [logger_sqlalchemy]
70
+ level = WARN
71
+ handlers =
72
+ qualname = sqlalchemy.engine
73
+
74
+ [logger_alembic]
75
+ level = INFO
76
+ handlers =
77
+ qualname = alembic
78
+
79
+ [handler_console]
80
+ class = StreamHandler
81
+ args = (sys.stderr,)
82
+ level = NOTSET
83
+ formatter = generic
84
+
85
+ [formatter_generic]
86
+ format = %(levelname)-5.5s [%(name)s] %(message)s
87
+ datefmt = %H:%M:%S
88
+
89
+
@@ -0,0 +1,3 @@
1
+ # Draft Backend
2
+
3
+
@@ -0,0 +1,85 @@
1
+ """Central data directory for Draft runtime artifacts.
2
+
3
+ All worktrees, logs, and evidence files live under a single central
4
+ directory (~/.draft/ by default) instead of polluting target repos.
5
+
6
+ Override with the DRAFT_DATA_DIR environment variable.
7
+ """
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ _DEFAULT_DATA_DIR = Path.home() / ".draft"
13
+
14
+
15
+ def get_data_dir() -> Path:
16
+ """Return the central data directory, creating it if needed.
17
+
18
+ Resolution order:
19
+ 1. DRAFT_DATA_DIR environment variable
20
+ 2. ~/.draft/
21
+ """
22
+ data_dir = Path(os.environ.get("DRAFT_DATA_DIR", str(_DEFAULT_DATA_DIR)))
23
+ data_dir.mkdir(parents=True, exist_ok=True)
24
+ return data_dir
25
+
26
+
27
+ def get_worktree_dir(board_id: str, ticket_id: str) -> Path:
28
+ """Return the worktree directory for a ticket.
29
+
30
+ Layout: {data_dir}/worktrees/{board_id}/{ticket_id}/
31
+ """
32
+ return get_data_dir() / "worktrees" / board_id / ticket_id
33
+
34
+
35
+ def get_worktrees_root() -> Path:
36
+ """Return the root worktrees directory.
37
+
38
+ Layout: {data_dir}/worktrees/
39
+ """
40
+ return get_data_dir() / "worktrees"
41
+
42
+
43
+ def get_log_path(job_id: str) -> Path:
44
+ """Return the log file path for a job.
45
+
46
+ Layout: {data_dir}/logs/{job_id}.log
47
+ """
48
+ logs_dir = get_logs_dir()
49
+ logs_dir.mkdir(parents=True, exist_ok=True)
50
+ return logs_dir / f"{job_id}.log"
51
+
52
+
53
+ def get_logs_dir() -> Path:
54
+ """Return the central logs directory.
55
+
56
+ Layout: {data_dir}/logs/
57
+ """
58
+ d = get_data_dir() / "logs"
59
+ d.mkdir(parents=True, exist_ok=True)
60
+ return d
61
+
62
+
63
+ def get_evidence_dir(job_id: str) -> Path:
64
+ """Return the evidence directory for a job.
65
+
66
+ Layout: {data_dir}/evidence/{job_id}/
67
+ """
68
+ d = get_data_dir() / "evidence" / job_id
69
+ d.mkdir(parents=True, exist_ok=True)
70
+ return d
71
+
72
+
73
+ def get_jobs_dir(job_id: str) -> Path:
74
+ """Return the job working directory.
75
+
76
+ Layout: {data_dir}/jobs/{job_id}/
77
+ """
78
+ d = get_data_dir() / "jobs" / job_id
79
+ d.mkdir(parents=True, exist_ok=True)
80
+ return d
81
+
82
+
83
+ # Legacy path constant for migration-period checks
84
+ LEGACY_DRAFT_DIR = ".draft"
85
+ LEGACY_WORKTREES_DIR = ".draft/worktrees"
@@ -0,0 +1,70 @@
1
+ """Database connection and session management for Draft."""
2
+
3
+ import os
4
+ from collections.abc import AsyncGenerator
5
+ from pathlib import Path
6
+
7
+ from sqlalchemy import event
8
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
9
+
10
+ from app.models.base import Base
11
+
12
+ # Get the backend directory (where this file lives is app/, so go up one level)
13
+ _BACKEND_DIR = Path(__file__).parent.parent.resolve()
14
+
15
+ # Database URL - defaults to SQLite file in the backend directory
16
+ # Use absolute path to ensure consistency across different working directories
17
+ DATABASE_URL = os.getenv(
18
+ "DATABASE_URL", f"sqlite+aiosqlite:///{_BACKEND_DIR}/kanban.db"
19
+ )
20
+
21
+ _is_sqlite = "sqlite" in DATABASE_URL
22
+
23
+ # Create async engine with SQLite-friendly settings
24
+ engine = create_async_engine(
25
+ DATABASE_URL,
26
+ echo=os.getenv("SQL_ECHO", "false").lower() == "true",
27
+ future=True,
28
+ connect_args={"timeout": 30} if _is_sqlite else {},
29
+ pool_pre_ping=True,
30
+ )
31
+
32
+
33
+ # Enable WAL mode for SQLite to prevent readers from blocking writers
34
+ if _is_sqlite:
35
+
36
+ @event.listens_for(engine.sync_engine, "connect")
37
+ def _set_sqlite_pragma(dbapi_conn, connection_record):
38
+ cursor = dbapi_conn.cursor()
39
+ cursor.execute("PRAGMA journal_mode=WAL")
40
+ cursor.execute("PRAGMA busy_timeout=30000")
41
+ cursor.close()
42
+
43
+
44
+ # Session factory
45
+ async_session_maker = async_sessionmaker(
46
+ engine,
47
+ class_=AsyncSession,
48
+ expire_on_commit=False,
49
+ autocommit=False,
50
+ autoflush=False,
51
+ )
52
+
53
+
54
+ async def init_db() -> None:
55
+ """Initialize the database by creating all tables."""
56
+ async with engine.begin() as conn:
57
+ await conn.run_sync(Base.metadata.create_all)
58
+
59
+
60
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
61
+ """Dependency for FastAPI to inject database sessions."""
62
+ async with async_session_maker() as session:
63
+ try:
64
+ yield session
65
+ await session.commit()
66
+ except Exception:
67
+ await session.rollback()
68
+ raise
69
+ finally:
70
+ await session.close()
@@ -0,0 +1,64 @@
1
+ """Synchronous database connection for Celery workers."""
2
+
3
+ import os
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from pathlib import Path
7
+
8
+ from sqlalchemy import create_engine, event
9
+ from sqlalchemy.orm import Session, sessionmaker
10
+
11
+ # Get the backend directory (where this file lives is app/, so go up one level)
12
+ _BACKEND_DIR = Path(__file__).parent.parent.resolve()
13
+
14
+ # Synchronous database URL - defaults to SQLite file in the backend directory
15
+ # Use absolute path to ensure worker finds the same database as the server
16
+ DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{_BACKEND_DIR}/kanban.db")
17
+ if DATABASE_URL.startswith("sqlite+aiosqlite"):
18
+ DATABASE_URL = DATABASE_URL.replace("sqlite+aiosqlite", "sqlite")
19
+
20
+ _is_sqlite = DATABASE_URL.startswith("sqlite")
21
+
22
+ # Create synchronous engine with SQLite-friendly settings
23
+ sync_engine = create_engine(
24
+ DATABASE_URL,
25
+ echo=os.getenv("SQL_ECHO", "false").lower() == "true",
26
+ future=True,
27
+ connect_args={"timeout": 30} if _is_sqlite else {},
28
+ pool_pre_ping=True,
29
+ )
30
+
31
+
32
+ # Enable WAL mode for SQLite to prevent readers from blocking writers
33
+ if _is_sqlite:
34
+
35
+ @event.listens_for(sync_engine, "connect")
36
+ def _set_sqlite_pragma(dbapi_conn, connection_record):
37
+ cursor = dbapi_conn.cursor()
38
+ cursor.execute("PRAGMA journal_mode=WAL")
39
+ cursor.execute("PRAGMA busy_timeout=30000")
40
+ cursor.close()
41
+
42
+
43
+ # Synchronous session factory
44
+ SyncSessionLocal = sessionmaker(
45
+ bind=sync_engine,
46
+ class_=Session,
47
+ expire_on_commit=False,
48
+ autocommit=False,
49
+ autoflush=False,
50
+ )
51
+
52
+
53
+ @contextmanager
54
+ def get_sync_db() -> Generator[Session, None, None]:
55
+ """Context manager for synchronous database sessions (used by Celery workers)."""
56
+ session = SyncSessionLocal()
57
+ try:
58
+ yield session
59
+ session.commit()
60
+ except Exception:
61
+ session.rollback()
62
+ raise
63
+ finally:
64
+ session.close()
@@ -0,0 +1,5 @@
1
+ """FastAPI dependency injection utilities."""
2
+
3
+ from app.dependencies.auth import get_current_user, get_optional_user
4
+
5
+ __all__ = ["get_current_user", "get_optional_user"]