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,267 @@
1
+ """Router for project settings (DB-backed via Board.config)."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+ from pydantic import BaseModel
8
+ from sqlalchemy import select
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from app.database import get_db
12
+ from app.models.board import Board
13
+ from app.services.config_service import DraftConfig, deep_merge_dicts
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ router = APIRouter(prefix="/settings", tags=["settings"])
18
+
19
+
20
+ class ExecuteConfigUpdate(BaseModel):
21
+ """Execute configuration update model."""
22
+
23
+ timeout: int | None = None
24
+ preferred_executor: str | None = None
25
+ executor_model: str | None = None
26
+
27
+
28
+ class SettingsUpdate(BaseModel):
29
+ """Global settings update model."""
30
+
31
+ execute_config: ExecuteConfigUpdate | None = None
32
+
33
+
34
+ class SettingsResponse(BaseModel):
35
+ """Global settings response model."""
36
+
37
+ execute_config: dict[str, Any]
38
+ board_id: str
39
+
40
+
41
+ # --- Planner config models ---
42
+
43
+
44
+ class PlannerConfigResponse(BaseModel):
45
+ """Planner configuration response."""
46
+
47
+ model: str
48
+ agent_path: str
49
+ timeout: int
50
+ preferred_executor: str # From execute_config, so frontend knows the CLI type
51
+
52
+
53
+ class PlannerConfigUpdate(BaseModel):
54
+ """Planner configuration update."""
55
+
56
+ model: str | None = None
57
+ agent_path: str | None = None
58
+
59
+
60
+ class PlannerHealthResponse(BaseModel):
61
+ """Planner health check response."""
62
+
63
+ status: str # "online" | "offline"
64
+ model: str
65
+ error: str | None = None
66
+
67
+
68
+ async def _resolve_board(db: AsyncSession, board_id: str | None) -> Board:
69
+ """Resolve a board by ID, or fall back to the first board."""
70
+ if board_id:
71
+ result = await db.execute(select(Board).where(Board.id == board_id))
72
+ board = result.scalar_one_or_none()
73
+ if not board:
74
+ raise HTTPException(status_code=404, detail=f"Board not found: {board_id}")
75
+ return board
76
+
77
+ result = await db.execute(select(Board).limit(1))
78
+ board = result.scalar_one_or_none()
79
+ if not board:
80
+ raise HTTPException(
81
+ status_code=400,
82
+ detail="No boards exist. Create a board first.",
83
+ )
84
+ return board
85
+
86
+
87
+ @router.get("", response_model=SettingsResponse)
88
+ async def get_global_settings(
89
+ board_id: str | None = Query(
90
+ None, description="Board ID (uses first board if omitted)"
91
+ ),
92
+ db: AsyncSession = Depends(get_db),
93
+ ):
94
+ """Get execute settings from board config (DB).
95
+
96
+ Returns:
97
+ Current execute_config from the board's config.
98
+ """
99
+ board = await _resolve_board(db, board_id)
100
+ config = DraftConfig.from_board_config(board.config)
101
+
102
+ return SettingsResponse(
103
+ execute_config={
104
+ "timeout": config.execute_config.timeout,
105
+ "preferred_executor": config.execute_config.preferred_executor,
106
+ "executor_model": config.execute_config.executor_model,
107
+ },
108
+ board_id=board.id,
109
+ )
110
+
111
+
112
+ @router.put("", response_model=SettingsResponse)
113
+ async def update_global_settings(
114
+ data: SettingsUpdate,
115
+ board_id: str | None = Query(
116
+ None, description="Board ID (uses first board if omitted)"
117
+ ),
118
+ db: AsyncSession = Depends(get_db),
119
+ ):
120
+ """Update execute settings in board config (DB).
121
+
122
+ Args:
123
+ data: Settings to update (partial update supported)
124
+
125
+ Returns:
126
+ Updated settings
127
+ """
128
+ board = await _resolve_board(db, board_id)
129
+
130
+ update_dict: dict[str, Any] = {}
131
+ if data.execute_config:
132
+ ec: dict[str, Any] = {}
133
+ if data.execute_config.timeout is not None:
134
+ ec["timeout"] = data.execute_config.timeout
135
+ if data.execute_config.preferred_executor is not None:
136
+ ec["preferred_executor"] = data.execute_config.preferred_executor
137
+ if data.execute_config.executor_model is not None:
138
+ ec["executor_model"] = data.execute_config.executor_model
139
+ if ec:
140
+ update_dict["execute_config"] = ec
141
+
142
+ if update_dict:
143
+ existing = board.config or {}
144
+ board.config = deep_merge_dicts(existing, update_dict)
145
+ await db.commit()
146
+ await db.refresh(board)
147
+
148
+ config = DraftConfig.from_board_config(board.config)
149
+ return SettingsResponse(
150
+ execute_config={
151
+ "timeout": config.execute_config.timeout,
152
+ "preferred_executor": config.execute_config.preferred_executor,
153
+ "executor_model": config.execute_config.executor_model,
154
+ },
155
+ board_id=board.id,
156
+ )
157
+
158
+
159
+ # ==================== Planner Config Endpoints ====================
160
+
161
+
162
+ @router.get("/planner", response_model=PlannerConfigResponse)
163
+ async def get_planner_config(
164
+ board_id: str | None = Query(
165
+ None, description="Board ID (uses first board if omitted)"
166
+ ),
167
+ db: AsyncSession = Depends(get_db),
168
+ ):
169
+ """Get current planner configuration from board config (DB)."""
170
+ board = await _resolve_board(db, board_id)
171
+ config = DraftConfig.from_board_config(board.config)
172
+ planner = config.planner_config
173
+
174
+ return PlannerConfigResponse(
175
+ model=planner.model,
176
+ agent_path=planner.agent_path,
177
+ timeout=planner.timeout,
178
+ preferred_executor=config.execute_config.preferred_executor,
179
+ )
180
+
181
+
182
+ @router.put("/planner", response_model=PlannerConfigResponse)
183
+ async def update_planner_config(
184
+ data: PlannerConfigUpdate,
185
+ board_id: str | None = Query(
186
+ None, description="Board ID (uses first board if omitted)"
187
+ ),
188
+ db: AsyncSession = Depends(get_db),
189
+ ):
190
+ """Update planner model and agent_path in board config (DB)."""
191
+ board = await _resolve_board(db, board_id)
192
+
193
+ update_dict: dict[str, Any] = {}
194
+ if data.model is not None:
195
+ update_dict["model"] = data.model
196
+ # When setting cli/<executor>, auto-sync agent_path to the executor name
197
+ if data.model.startswith("cli/") and data.agent_path is None:
198
+ update_dict["agent_path"] = data.model.removeprefix("cli/")
199
+ if data.agent_path is not None:
200
+ update_dict["agent_path"] = data.agent_path
201
+
202
+ if update_dict:
203
+ existing = board.config or {}
204
+ board.config = deep_merge_dicts(existing, {"planner_config": update_dict})
205
+ await db.commit()
206
+ await db.refresh(board)
207
+
208
+ config = DraftConfig.from_board_config(board.config)
209
+ planner = config.planner_config
210
+
211
+ return PlannerConfigResponse(
212
+ model=planner.model,
213
+ agent_path=planner.agent_path,
214
+ timeout=planner.timeout,
215
+ preferred_executor=config.execute_config.preferred_executor,
216
+ )
217
+
218
+
219
+ @router.get("/planner/check", response_model=PlannerHealthResponse)
220
+ async def check_planner_health(
221
+ board_id: str | None = Query(
222
+ None, description="Board ID (uses first board if omitted)"
223
+ ),
224
+ db: AsyncSession = Depends(get_db),
225
+ ):
226
+ """Test if the configured planner can work.
227
+
228
+ For CLI models (cli/claude): checks if the CLI binary is available.
229
+ For API models: makes a minimal test call to verify credentials.
230
+ """
231
+ import shutil
232
+
233
+ board = await _resolve_board(db, board_id)
234
+ config = DraftConfig.from_board_config(board.config)
235
+ planner = config.planner_config
236
+ model = planner.model
237
+
238
+ # CLI mode: check if the agent binary exists
239
+ if model.startswith("cli/"):
240
+ agent_path = planner.get_agent_path()
241
+ found = shutil.which(agent_path)
242
+ if found:
243
+ logger.info(f"Planner CLI health check passed: {agent_path} -> {found}")
244
+ return PlannerHealthResponse(status="online", model=model)
245
+ else:
246
+ return PlannerHealthResponse(
247
+ status="offline",
248
+ model=model,
249
+ error=f"CLI not found: {agent_path}. Install it or add it to PATH.",
250
+ )
251
+
252
+ # API mode: make a minimal LLM call
253
+ from app.services.llm_service import LLMService
254
+
255
+ try:
256
+ llm = LLMService(planner)
257
+ llm.call_completion(
258
+ messages=[{"role": "user", "content": 'Reply with exactly: {"ok":true}'}],
259
+ max_tokens=20,
260
+ timeout=15,
261
+ json_mode=True,
262
+ )
263
+ logger.info(f"Planner API health check passed: model={model}")
264
+ return PlannerHealthResponse(status="online", model=model)
265
+ except Exception as e:
266
+ logger.warning(f"Planner health check failed: {e}")
267
+ return PlannerHealthResponse(status="offline", model=model, error=str(e))