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,324 @@
1
+ """Router for executor management and listing."""
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, Query
6
+ from sqlalchemy import select
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+ from app.database import get_db
10
+ from app.executors.registry import ExecutorRegistry
11
+ from app.models.board import Board
12
+ from app.services.config_service import DraftConfig, deep_merge_dicts
13
+
14
+ router = APIRouter(prefix="/executors", tags=["executors"])
15
+
16
+
17
+ @router.get("/available", response_model=list[dict[str, Any]])
18
+ async def list_available_executors():
19
+ """List all available executors (both installed and not installed).
20
+
21
+ Returns:
22
+ List of executor metadata with availability status
23
+ """
24
+ try:
25
+ # Get all registered executors via public API
26
+ all_executors = []
27
+
28
+ for metadata in ExecutorRegistry.list_all():
29
+ adapter = ExecutorRegistry.get(metadata.name)
30
+ is_available = await adapter.is_available()
31
+
32
+ executor_dict = {
33
+ "name": metadata.name,
34
+ "display_name": metadata.display_name,
35
+ "version": metadata.version,
36
+ "capabilities": [cap.value for cap in metadata.capabilities],
37
+ "config_schema": metadata.config_schema,
38
+ "documentation_url": metadata.documentation_url,
39
+ "author": metadata.author,
40
+ "license": metadata.license,
41
+ "available": is_available,
42
+ }
43
+
44
+ all_executors.append(executor_dict)
45
+
46
+ return all_executors
47
+
48
+ except Exception as e:
49
+ raise HTTPException(
50
+ status_code=500, detail=f"Failed to list executors: {str(e)}"
51
+ )
52
+
53
+
54
+ @router.get("/{executor_name}/metadata", response_model=dict[str, Any])
55
+ async def get_executor_metadata(executor_name: str):
56
+ """Get metadata for a specific executor.
57
+
58
+ Args:
59
+ executor_name: Name of the executor
60
+
61
+ Returns:
62
+ Executor metadata
63
+ """
64
+ try:
65
+ adapter = ExecutorRegistry.get(executor_name)
66
+ metadata = adapter.get_metadata()
67
+ is_available = await adapter.is_available()
68
+
69
+ return {
70
+ "name": metadata.name,
71
+ "display_name": metadata.display_name,
72
+ "version": metadata.version,
73
+ "capabilities": [cap.value for cap in metadata.capabilities],
74
+ "config_schema": metadata.config_schema,
75
+ "documentation_url": metadata.documentation_url,
76
+ "author": metadata.author,
77
+ "license": metadata.license,
78
+ "available": is_available,
79
+ }
80
+
81
+ except ValueError as e:
82
+ raise HTTPException(status_code=404, detail=str(e))
83
+ except Exception as e:
84
+ raise HTTPException(status_code=500, detail=f"Failed to get metadata: {str(e)}")
85
+
86
+
87
+ @router.get("/{executor_name}/models", response_model=list[dict[str, str]])
88
+ async def list_executor_models(executor_name: str):
89
+ """List available models for a given executor type.
90
+
91
+ Returns a list of model options that can be used with this executor.
92
+ """
93
+ # Model options per executor type
94
+ models_by_executor: dict[str, list[dict[str, str]]] = {
95
+ "claude": [
96
+ {
97
+ "id": "auto",
98
+ "name": "Auto (recommended)",
99
+ "description": "Automatically select the best model",
100
+ },
101
+ {
102
+ "id": "claude-sonnet-4-20250514",
103
+ "name": "Claude Sonnet 4",
104
+ "description": "Fast and capable",
105
+ },
106
+ {
107
+ "id": "claude-opus-4-20250514",
108
+ "name": "Claude Opus 4",
109
+ "description": "Most capable model",
110
+ },
111
+ ],
112
+ "cursor-agent": [
113
+ {
114
+ "id": "auto",
115
+ "name": "Auto (recommended)",
116
+ "description": "Automatically select the best model",
117
+ },
118
+ ],
119
+ "cursor": [
120
+ {
121
+ "id": "auto",
122
+ "name": "Auto (recommended)",
123
+ "description": "Uses Cursor IDE model selection",
124
+ },
125
+ ],
126
+ }
127
+
128
+ models = models_by_executor.get(executor_name)
129
+ if models is None:
130
+ raise HTTPException(
131
+ status_code=404,
132
+ detail=f"Unknown executor: {executor_name}",
133
+ )
134
+ return models
135
+
136
+
137
+ @router.get("/{executor_name}/available")
138
+ async def check_executor_available(executor_name: str):
139
+ """Check if a specific executor is available (installed).
140
+
141
+ Args:
142
+ executor_name: Name of the executor
143
+
144
+ Returns:
145
+ Dict with availability status
146
+ """
147
+ try:
148
+ adapter = ExecutorRegistry.get(executor_name)
149
+ is_available = await adapter.is_available()
150
+
151
+ return {"name": executor_name, "available": is_available}
152
+
153
+ except ValueError as e:
154
+ raise HTTPException(status_code=404, detail=str(e))
155
+ except Exception as e:
156
+ raise HTTPException(
157
+ status_code=500, detail=f"Failed to check availability: {str(e)}"
158
+ )
159
+
160
+
161
+ @router.get("/{executor_name}/setup")
162
+ async def get_executor_setup(executor_name: str):
163
+ """Get setup instructions and availability diagnostics for an executor.
164
+
165
+ Returns detailed information about whether the executor is installed,
166
+ what issues exist, and how to set it up.
167
+
168
+ Args:
169
+ executor_name: Name of the executor
170
+
171
+ Returns:
172
+ Dict with availability diagnostics and setup instructions
173
+ """
174
+ try:
175
+ adapter = ExecutorRegistry.get(executor_name)
176
+ diagnostics = await adapter.check_availability()
177
+ metadata = adapter.get_metadata()
178
+
179
+ return {
180
+ "name": metadata.name,
181
+ "display_name": metadata.display_name,
182
+ **diagnostics,
183
+ }
184
+
185
+ except ValueError as e:
186
+ raise HTTPException(status_code=404, detail=str(e))
187
+ except Exception as e:
188
+ raise HTTPException(status_code=500, detail=f"Failed to check setup: {str(e)}")
189
+
190
+
191
+ async def _resolve_board_for_executors(db: AsyncSession, board_id: str | None) -> Board:
192
+ """Resolve a board by ID, or fall back to the first board."""
193
+ if board_id:
194
+ result = await db.execute(select(Board).where(Board.id == board_id))
195
+ board = result.scalar_one_or_none()
196
+ if not board:
197
+ raise HTTPException(status_code=404, detail=f"Board not found: {board_id}")
198
+ return board
199
+
200
+ result = await db.execute(select(Board).limit(1))
201
+ board = result.scalar_one_or_none()
202
+ if not board:
203
+ raise HTTPException(
204
+ status_code=400,
205
+ detail="No boards exist. Create a board first.",
206
+ )
207
+ return board
208
+
209
+
210
+ @router.get("/profiles", response_model=list[dict[str, Any]])
211
+ async def list_executor_profiles(
212
+ board_id: str | None = Query(
213
+ None, description="Board ID (uses first board if omitted)"
214
+ ),
215
+ db: AsyncSession = Depends(get_db),
216
+ ):
217
+ """List all configured executor profiles from board config (DB).
218
+
219
+ Returns:
220
+ List of executor profile configurations
221
+ """
222
+ board = await _resolve_board_for_executors(db, board_id)
223
+ config = DraftConfig.from_board_config(board.config)
224
+ profiles = config.executor_profiles
225
+
226
+ return [
227
+ {
228
+ "name": profile.name,
229
+ "executor_type": profile.executor_type,
230
+ "timeout": profile.timeout,
231
+ "extra_flags": profile.extra_flags,
232
+ "model": profile.model,
233
+ "env": profile.env,
234
+ }
235
+ for profile in profiles.values()
236
+ ]
237
+
238
+
239
+ @router.get("/profiles/{profile_name}", response_model=dict[str, Any])
240
+ async def get_executor_profile(
241
+ profile_name: str,
242
+ board_id: str | None = Query(
243
+ None, description="Board ID (uses first board if omitted)"
244
+ ),
245
+ db: AsyncSession = Depends(get_db),
246
+ ):
247
+ """Get a specific executor profile by name.
248
+
249
+ Args:
250
+ profile_name: Name of the profile
251
+
252
+ Returns:
253
+ Executor profile configuration
254
+ """
255
+ board = await _resolve_board_for_executors(db, board_id)
256
+ config = DraftConfig.from_board_config(board.config)
257
+ profile = config.executor_profiles.get(profile_name)
258
+
259
+ if not profile:
260
+ raise HTTPException(
261
+ status_code=404,
262
+ detail=f"Executor profile '{profile_name}' not found",
263
+ )
264
+
265
+ return {
266
+ "name": profile.name,
267
+ "executor_type": profile.executor_type,
268
+ "timeout": profile.timeout,
269
+ "extra_flags": profile.extra_flags,
270
+ "model": profile.model,
271
+ "env": profile.env,
272
+ }
273
+
274
+
275
+ @router.put("/profiles", response_model=list[dict[str, Any]])
276
+ async def save_executor_profiles(
277
+ profiles: list[dict[str, Any]],
278
+ board_id: str | None = Query(
279
+ None, description="Board ID (uses first board if omitted)"
280
+ ),
281
+ db: AsyncSession = Depends(get_db),
282
+ ):
283
+ """Save executor profiles to board config (DB).
284
+
285
+ Replaces all profiles with the provided list.
286
+ """
287
+ board = await _resolve_board_for_executors(db, board_id)
288
+
289
+ # Build profiles dict for storage
290
+ profiles_dict: dict[str, Any] = {}
291
+ for p in profiles:
292
+ name = p.get("name", "").strip()
293
+ if not name:
294
+ continue
295
+ entry: dict[str, Any] = {}
296
+ if p.get("executor_type"):
297
+ entry["executor_type"] = p["executor_type"]
298
+ if p.get("timeout"):
299
+ entry["timeout"] = int(p["timeout"])
300
+ if p.get("extra_flags"):
301
+ entry["extra_flags"] = p["extra_flags"]
302
+ if p.get("model"):
303
+ entry["model"] = p["model"]
304
+ if p.get("env"):
305
+ entry["env"] = p["env"]
306
+ profiles_dict[name] = entry
307
+
308
+ existing = board.config or {}
309
+ board.config = deep_merge_dicts(existing, {"executor_profiles": profiles_dict})
310
+ await db.commit()
311
+ await db.refresh(board)
312
+
313
+ config = DraftConfig.from_board_config(board.config)
314
+ return [
315
+ {
316
+ "name": prof.name,
317
+ "executor_type": prof.executor_type,
318
+ "timeout": prof.timeout,
319
+ "extra_flags": prof.extra_flags,
320
+ "model": prof.model,
321
+ "env": prof.env,
322
+ }
323
+ for prof in config.executor_profiles.values()
324
+ ]