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,264 @@
1
+ """Service for generating and managing merge readiness checklists."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any
6
+
7
+ from sqlalchemy import select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from sqlalchemy.orm import selectinload
10
+
11
+ from app.exceptions import ResourceNotFoundError
12
+ from app.models.evidence import Evidence
13
+ from app.models.job import Job
14
+ from app.models.merge_checklist import MergeChecklist
15
+ from app.models.ticket import Ticket
16
+ from app.state_machine import JobStatus, TicketState
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MergeChecklistService:
22
+ """Generate and track merge readiness checklist."""
23
+
24
+ def __init__(self, db: AsyncSession):
25
+ self.db = db
26
+
27
+ async def generate_or_update_checklist(self, goal_id: str) -> MergeChecklist:
28
+ """Generate or update the merge checklist for a goal.
29
+
30
+ Automatically checks:
31
+ - All tests passed
32
+ - Files/lines changed count
33
+ - Total cost
34
+ - Budget status
35
+
36
+ Returns:
37
+ MergeChecklist instance
38
+ """
39
+ # Get existing checklist or create new
40
+ result = await self.db.execute(
41
+ select(MergeChecklist).where(MergeChecklist.goal_id == goal_id)
42
+ )
43
+ checklist = result.scalar_one_or_none()
44
+
45
+ if not checklist:
46
+ checklist = MergeChecklist(goal_id=goal_id)
47
+ self.db.add(checklist)
48
+
49
+ # Run automatic checks
50
+ await self._update_auto_checks(checklist, goal_id)
51
+
52
+ # Generate rollback plan if not exists
53
+ if not checklist.rollback_plan_json:
54
+ rollback_plan = await self._generate_rollback_plan(goal_id)
55
+ checklist.rollback_plan_json = json.dumps(rollback_plan)
56
+ checklist.risk_level = rollback_plan["risk_level"]
57
+
58
+ # Update ready status
59
+ checklist.ready_to_merge = checklist.is_ready_to_merge()
60
+
61
+ await self.db.flush()
62
+ await self.db.refresh(checklist)
63
+
64
+ return checklist
65
+
66
+ async def _update_auto_checks(self, checklist: MergeChecklist, goal_id: str):
67
+ """Update automatic checks by querying system state."""
68
+ # Get all tickets for this goal
69
+ result = await self.db.execute(
70
+ select(Ticket)
71
+ .where(Ticket.goal_id == goal_id)
72
+ .options(
73
+ selectinload(Ticket.jobs).selectinload(Job.evidence),
74
+ selectinload(Ticket.revisions),
75
+ )
76
+ )
77
+ tickets = list(result.scalars().all())
78
+
79
+ # Check 1: All tests passed
80
+ checklist.all_tests_passed = await self._check_all_tests_passed(tickets)
81
+
82
+ # Check 2: Count files and lines changed
83
+ file_stats = await self._count_changes(tickets)
84
+ checklist.total_files_changed = file_stats["files"]
85
+ checklist.total_lines_changed = file_stats["lines"]
86
+
87
+ # Check 3: Calculate total cost
88
+ checklist.total_cost_usd = await self._calculate_cost(goal_id)
89
+
90
+ # Check 4: Check budget
91
+ checklist.budget_exceeded = await self._check_budget_exceeded(goal_id)
92
+
93
+ async def _check_all_tests_passed(self, tickets: list[Ticket]) -> bool:
94
+ """Check if all verification jobs succeeded."""
95
+ for ticket in tickets:
96
+ # Skip tickets that aren't done yet
97
+ if ticket.state != TicketState.DONE.value:
98
+ return False
99
+
100
+ # Find most recent verify job
101
+ verify_jobs = [j for j in ticket.jobs if j.job_type == "verify"]
102
+ if not verify_jobs:
103
+ return False
104
+
105
+ latest_verify = max(verify_jobs, key=lambda j: j.created_at)
106
+ if latest_verify.status != JobStatus.SUCCEEDED.value:
107
+ return False
108
+
109
+ return True
110
+
111
+ async def _count_changes(self, tickets: list[Ticket]) -> dict[str, int]:
112
+ """Count total files and lines changed across all tickets."""
113
+ total_files = set()
114
+ total_lines = 0
115
+
116
+ for ticket in tickets:
117
+ # Get diffs from revisions
118
+ for revision in ticket.revisions:
119
+ if revision.diff_stat_evidence_id:
120
+ # Parse diff stat from evidence
121
+ stat_result = await self.db.execute(
122
+ select(Evidence).where(
123
+ Evidence.id == revision.diff_stat_evidence_id
124
+ )
125
+ )
126
+ stat_evidence = stat_result.scalar_one_or_none()
127
+
128
+ if stat_evidence and stat_evidence.stdout_path:
129
+ # Parse diff stat (format: "X files changed, Y insertions(+), Z deletions(-)")
130
+ try:
131
+ with open(stat_evidence.stdout_path) as f:
132
+ stat_line = f.read().strip()
133
+ # Simple parsing
134
+ if (
135
+ "files changed" in stat_line
136
+ or "file changed" in stat_line
137
+ ):
138
+ parts = stat_line.split(",")
139
+ files_part = parts[0].strip().split()[0]
140
+ total_files.add(f"{ticket.id}:{files_part}")
141
+
142
+ # Count insertions and deletions
143
+ for part in parts[1:]:
144
+ if "insertion" in part or "deletion" in part:
145
+ count = int(part.strip().split()[0])
146
+ total_lines += count
147
+ except Exception as e:
148
+ logger.warning(f"Failed to parse diff stat: {e}")
149
+
150
+ return {"files": len(total_files), "lines": total_lines}
151
+
152
+ async def _calculate_cost(self, goal_id: str) -> float:
153
+ """Calculate total LLM API cost for all tickets."""
154
+ # TODO: Aggregate from agent_sessions table
155
+ return 0.0
156
+
157
+ async def _check_budget_exceeded(self, goal_id: str) -> bool:
158
+ """Check if spending exceeded budget."""
159
+ # TODO: Check against cost_budget table
160
+ return False
161
+
162
+ async def _generate_rollback_plan(self, goal_id: str) -> dict[str, Any]:
163
+ """Generate rollback plan for all changes."""
164
+ result = await self.db.execute(
165
+ select(Ticket)
166
+ .where(Ticket.goal_id == goal_id)
167
+ .where(Ticket.state == TicketState.DONE.value)
168
+ )
169
+ tickets = list(result.scalars().all())
170
+
171
+ steps = []
172
+
173
+ # Step 1: Git revert for all merged changes
174
+ if tickets:
175
+ steps.append(
176
+ {
177
+ "order": 1,
178
+ "type": "git",
179
+ "description": f"Revert all commits for {len(tickets)} tickets",
180
+ "command": "git log --grep='ticket_id' --oneline | awk '{print $1}' | xargs git revert --no-commit",
181
+ "is_automated": True,
182
+ "risk": "low",
183
+ }
184
+ )
185
+
186
+ # Step 2: Check for database migrations
187
+ has_migrations = False # TODO: Detect if any ticket modified migrations
188
+ if has_migrations:
189
+ steps.append(
190
+ {
191
+ "order": 2,
192
+ "type": "migration",
193
+ "description": "Rollback database migrations",
194
+ "command": "alembic downgrade -1",
195
+ "is_automated": False,
196
+ "risk": "high",
197
+ }
198
+ )
199
+
200
+ # Step 3: Cache invalidation
201
+ steps.append(
202
+ {
203
+ "order": 3,
204
+ "type": "cache",
205
+ "description": "Clear application caches",
206
+ "command": "redis-cli FLUSHDB",
207
+ "is_automated": True,
208
+ "risk": "low",
209
+ }
210
+ )
211
+
212
+ # Assess overall risk
213
+ risk_level = "high" if has_migrations else "low"
214
+
215
+ return {
216
+ "steps": steps,
217
+ "risk_level": risk_level,
218
+ "estimated_time": "5-10 minutes",
219
+ "requires_human": any(s["risk"] == "high" for s in steps),
220
+ }
221
+
222
+ async def update_manual_check(
223
+ self, checklist_id: str, check_name: str, value: bool
224
+ ) -> MergeChecklist:
225
+ """Update a manual checklist item.
226
+
227
+ Args:
228
+ checklist_id: Checklist ID
229
+ check_name: Name of check (code_reviewed, no_sensitive_data, etc.)
230
+ value: New value
231
+
232
+ Returns:
233
+ Updated checklist
234
+ """
235
+ result = await self.db.execute(
236
+ select(MergeChecklist).where(MergeChecklist.id == checklist_id)
237
+ )
238
+ checklist = result.scalar_one_or_none()
239
+
240
+ if not checklist:
241
+ raise ResourceNotFoundError("MergeChecklist", checklist_id)
242
+
243
+ # Update the field
244
+ if hasattr(checklist, check_name):
245
+ setattr(checklist, check_name, value)
246
+ else:
247
+ raise ValueError(f"Unknown check: {check_name}")
248
+
249
+ # Recalculate ready status
250
+ checklist.ready_to_merge = checklist.is_ready_to_merge()
251
+
252
+ await self.db.flush()
253
+ await self.db.refresh(checklist)
254
+
255
+ return checklist
256
+
257
+ async def get_checklist_by_goal(self, goal_id: str) -> MergeChecklist | None:
258
+ """Get checklist for a goal."""
259
+ result = await self.db.execute(
260
+ select(MergeChecklist)
261
+ .where(MergeChecklist.goal_id == goal_id)
262
+ .options(selectinload(MergeChecklist.goal))
263
+ )
264
+ return result.scalar_one_or_none()