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,243 @@
1
+ """Project template registry with pre-configured board setups."""
2
+
3
+ from typing import TypedDict
4
+
5
+
6
+ class TemplateGoal(TypedDict, total=False):
7
+ """A starter goal for a template."""
8
+
9
+ title: str
10
+ description: str
11
+
12
+
13
+ class ProjectTemplate(TypedDict):
14
+ """A project template with pre-configured settings and starter goals."""
15
+
16
+ id: str
17
+ name: str
18
+ description: str
19
+ icon: str
20
+ category: str
21
+ config: dict
22
+ starter_goals: list[TemplateGoal]
23
+ tags: list[str]
24
+
25
+
26
+ TEMPLATES: list[ProjectTemplate] = [
27
+ {
28
+ "id": "web-app",
29
+ "name": "Web Application",
30
+ "description": "Modern web app with React, Next.js, or Vue. Optimized for UI development.",
31
+ "icon": "🌐",
32
+ "category": "Frontend",
33
+ "config": {
34
+ "execute_config": {
35
+ "executor_model": "sonnet-4.5",
36
+ "timeout": 300,
37
+ }
38
+ },
39
+ "starter_goals": [
40
+ {
41
+ "title": "Set up project structure",
42
+ "description": "Initialize the web app with proper folder structure, routing, and build configuration.",
43
+ },
44
+ {
45
+ "title": "Implement responsive layout",
46
+ "description": "Create a mobile-first responsive layout with navigation and theming support.",
47
+ },
48
+ ],
49
+ "tags": ["react", "nextjs", "vue", "frontend", "ui"],
50
+ },
51
+ {
52
+ "id": "api-service",
53
+ "name": "API Service",
54
+ "description": "REST or GraphQL API with FastAPI, Express, or Django. Optimized for backend development.",
55
+ "icon": "🔌",
56
+ "category": "Backend",
57
+ "config": {
58
+ "execute_config": {
59
+ "executor_model": "sonnet-4.5",
60
+ "timeout": 400,
61
+ }
62
+ },
63
+ "starter_goals": [
64
+ {
65
+ "title": "Set up API framework",
66
+ "description": "Initialize the API with routing, middleware, and database connection.",
67
+ },
68
+ {
69
+ "title": "Add authentication",
70
+ "description": "Implement JWT or OAuth authentication with proper security.",
71
+ },
72
+ {
73
+ "title": "Add API documentation",
74
+ "description": "Generate OpenAPI/Swagger docs with examples and schemas.",
75
+ },
76
+ ],
77
+ "tags": ["api", "fastapi", "express", "django", "backend", "rest", "graphql"],
78
+ },
79
+ {
80
+ "id": "mobile-app",
81
+ "name": "Mobile App",
82
+ "description": "Cross-platform mobile app with React Native or Flutter. Optimized for mobile development.",
83
+ "icon": "📱",
84
+ "category": "Mobile",
85
+ "config": {
86
+ "execute_config": {
87
+ "executor_model": "sonnet-4.5",
88
+ "timeout": 350,
89
+ }
90
+ },
91
+ "starter_goals": [
92
+ {
93
+ "title": "Set up navigation",
94
+ "description": "Configure screen navigation and routing for iOS and Android.",
95
+ },
96
+ {
97
+ "title": "Implement offline support",
98
+ "description": "Add local data persistence and offline-first architecture.",
99
+ },
100
+ ],
101
+ "tags": ["mobile", "react-native", "flutter", "ios", "android"],
102
+ },
103
+ {
104
+ "id": "data-pipeline",
105
+ "name": "Data Pipeline",
106
+ "description": "ETL/analytics pipeline with longer timeout for data processing tasks.",
107
+ "icon": "📊",
108
+ "category": "Data",
109
+ "config": {
110
+ "execute_config": {
111
+ "executor_model": "sonnet-4.5",
112
+ "timeout": 600, # 10 minutes for data processing
113
+ }
114
+ },
115
+ "starter_goals": [
116
+ {
117
+ "title": "Set up data sources",
118
+ "description": "Configure connections to databases, APIs, or file sources.",
119
+ },
120
+ {
121
+ "title": "Build transformation pipeline",
122
+ "description": "Implement data cleaning, validation, and transformation logic.",
123
+ },
124
+ {
125
+ "title": "Add monitoring and alerts",
126
+ "description": "Set up pipeline monitoring, error handling, and alerting.",
127
+ },
128
+ ],
129
+ "tags": ["data", "etl", "analytics", "pipeline", "airflow", "spark"],
130
+ },
131
+ {
132
+ "id": "docs-site",
133
+ "name": "Documentation Site",
134
+ "description": "Documentation with Docusaurus, MkDocs, or VitePress. Optimized for content writing.",
135
+ "icon": "📚",
136
+ "category": "Content",
137
+ "config": {
138
+ "execute_config": {
139
+ "executor_model": "sonnet-4.5",
140
+ "timeout": 250,
141
+ }
142
+ },
143
+ "starter_goals": [
144
+ {
145
+ "title": "Set up docs structure",
146
+ "description": "Organize content into sections with proper navigation.",
147
+ },
148
+ {
149
+ "title": "Add search functionality",
150
+ "description": "Implement full-text search across documentation.",
151
+ },
152
+ ],
153
+ "tags": ["docs", "documentation", "docusaurus", "mkdocs", "vitepress"],
154
+ },
155
+ {
156
+ "id": "library",
157
+ "name": "Library/Package",
158
+ "description": "Reusable library or NPM/PyPI package. Optimized for library development.",
159
+ "icon": "📦",
160
+ "category": "Library",
161
+ "config": {
162
+ "execute_config": {
163
+ "executor_model": "sonnet-4.5",
164
+ "timeout": 300,
165
+ }
166
+ },
167
+ "starter_goals": [
168
+ {
169
+ "title": "Set up build and packaging",
170
+ "description": "Configure build tools, TypeScript/types, and package.json/pyproject.toml.",
171
+ },
172
+ {
173
+ "title": "Add comprehensive tests",
174
+ "description": "Implement unit tests with high coverage and CI integration.",
175
+ },
176
+ {
177
+ "title": "Create usage examples",
178
+ "description": "Write clear examples and API documentation for library users.",
179
+ },
180
+ ],
181
+ "tags": ["library", "package", "npm", "pypi", "sdk"],
182
+ },
183
+ {
184
+ "id": "ml-model",
185
+ "name": "Machine Learning",
186
+ "description": "ML/AI model with training pipelines. Extended timeout for model training.",
187
+ "icon": "🤖",
188
+ "category": "AI/ML",
189
+ "config": {
190
+ "execute_config": {
191
+ "executor_model": "sonnet-4.5",
192
+ "timeout": 900, # 15 minutes for training
193
+ }
194
+ },
195
+ "starter_goals": [
196
+ {
197
+ "title": "Set up training pipeline",
198
+ "description": "Configure data loading, model architecture, and training loop.",
199
+ },
200
+ {
201
+ "title": "Add experiment tracking",
202
+ "description": "Integrate MLflow, Weights & Biases, or similar for experiment tracking.",
203
+ },
204
+ {
205
+ "title": "Implement model evaluation",
206
+ "description": "Create evaluation metrics, validation splits, and model comparison.",
207
+ },
208
+ ],
209
+ "tags": [
210
+ "ml",
211
+ "ai",
212
+ "machine-learning",
213
+ "pytorch",
214
+ "tensorflow",
215
+ "scikit-learn",
216
+ ],
217
+ },
218
+ {
219
+ "id": "blank",
220
+ "name": "Blank Project",
221
+ "description": "Start from scratch with default settings. No starter goals.",
222
+ "icon": "✨",
223
+ "category": "Other",
224
+ "config": {
225
+ "execute_config": {
226
+ "executor_model": "sonnet-4.5",
227
+ "timeout": 300,
228
+ }
229
+ },
230
+ "starter_goals": [],
231
+ "tags": ["blank", "custom"],
232
+ },
233
+ ]
234
+
235
+
236
+ def get_template(template_id: str) -> ProjectTemplate | None:
237
+ """Get a template by ID."""
238
+ return next((t for t in TEMPLATES if t["id"] == template_id), None)
239
+
240
+
241
+ def list_templates() -> list[ProjectTemplate]:
242
+ """List all available templates."""
243
+ return TEMPLATES
@@ -0,0 +1,5 @@
1
+ """Utility modules for the backend application."""
2
+
3
+ from app.utils.artifact_reader import read_artifact
4
+
5
+ __all__ = ["read_artifact"]
@@ -0,0 +1,87 @@
1
+ """Safe artifact reading utilities.
2
+
3
+ Security Policy:
4
+ - Only reads files under central data dir or <repo_root>/.draft (legacy)
5
+ - Rejects absolute paths (unless under central data dir)
6
+ - Resolves canonical paths (follows symlinks)
7
+ - Caps file size to prevent memory exhaustion
8
+ """
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ # Maximum artifact file size to read (2MB)
14
+ MAX_ARTIFACT_BYTES = 2_000_000
15
+
16
+
17
+ def _is_under(target: Path, allowed_root: Path) -> bool:
18
+ """Check if target path is under allowed_root using canonical paths."""
19
+ try:
20
+ common = os.path.commonpath(
21
+ [str(target.resolve(strict=False)), str(allowed_root.resolve(strict=False))]
22
+ )
23
+ except ValueError:
24
+ return False
25
+ return common == str(allowed_root.resolve(strict=False))
26
+
27
+
28
+ def _read_with_cap(target: Path) -> str | None:
29
+ """Read a file with size cap to prevent memory exhaustion."""
30
+ if not target.is_file():
31
+ return None
32
+ try:
33
+ size = target.stat().st_size
34
+ if size > MAX_ARTIFACT_BYTES:
35
+ with target.open("rb") as f:
36
+ data = f.read(MAX_ARTIFACT_BYTES)
37
+ return data.decode("utf-8", errors="replace") + "\n\n[truncated]"
38
+ return target.read_text(encoding="utf-8", errors="replace")
39
+ except OSError:
40
+ return None
41
+
42
+
43
+ def read_artifact(repo_root: Path, relpath: str | None) -> str | None:
44
+ """Safely read an artifact file, enforcing security constraints.
45
+
46
+ Security Policy:
47
+ - Accepts absolute paths under central data dir (~/.draft/)
48
+ - Accepts relative paths under <repo_root>/.draft (legacy)
49
+ - Resolves canonical path (follows symlinks)
50
+ - Caps file size to prevent memory exhaustion
51
+
52
+ Args:
53
+ repo_root: Absolute path to the repository root
54
+ relpath: Path to the artifact (absolute under data dir, or relative)
55
+
56
+ Returns:
57
+ File content if safe and exists, None otherwise
58
+ """
59
+ if not relpath:
60
+ return None
61
+
62
+ from app.data_dir import get_data_dir
63
+
64
+ rel = Path(relpath)
65
+
66
+ # If absolute path, check if it's under the central data dir
67
+ if rel.is_absolute():
68
+ data_dir = get_data_dir()
69
+ if _is_under(rel, data_dir):
70
+ return _read_with_cap(rel)
71
+ return None
72
+
73
+ # Try central data dir first (new paths)
74
+ data_dir = get_data_dir()
75
+ target = (data_dir / rel).resolve(strict=False)
76
+ if _is_under(target, data_dir):
77
+ result = _read_with_cap(target)
78
+ if result is not None:
79
+ return result
80
+
81
+ # Fall back to legacy <repo_root>/.draft
82
+ allowed_root = (repo_root / ".draft").resolve(strict=False)
83
+ target = (repo_root / rel).resolve(strict=False)
84
+ if _is_under(target, allowed_root):
85
+ return _read_with_cap(target)
86
+
87
+ return None
@@ -0,0 +1,229 @@
1
+ """Circuit breaker pattern for resilient external API calls.
2
+
3
+ Prevents cascading failures by temporarily stopping requests to a failing service,
4
+ giving it time to recover.
5
+ """
6
+
7
+ import logging
8
+ import threading
9
+ from collections.abc import Callable
10
+ from datetime import datetime, timedelta
11
+ from enum import StrEnum
12
+ from typing import TypeVar
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class CircuitState(StrEnum):
20
+ """Circuit breaker states."""
21
+
22
+ CLOSED = "closed" # Normal operation
23
+ OPEN = "open" # Failing, reject requests immediately
24
+ HALF_OPEN = "half_open" # Testing if service recovered
25
+
26
+
27
+ class CircuitBreakerError(Exception):
28
+ """Raised when circuit breaker is open."""
29
+
30
+ pass
31
+
32
+
33
+ class CircuitBreaker:
34
+ """Circuit breaker for external API calls with automatic recovery.
35
+
36
+ States:
37
+ - CLOSED: Normal operation, requests pass through
38
+ - OPEN: Too many failures, reject all requests immediately
39
+ - HALF_OPEN: Testing recovery, allow limited requests
40
+
41
+ Transitions:
42
+ - CLOSED -> OPEN: After failure_threshold consecutive failures
43
+ - OPEN -> HALF_OPEN: After timeout_seconds elapsed
44
+ - HALF_OPEN -> CLOSED: After success_threshold consecutive successes
45
+ - HALF_OPEN -> OPEN: On any failure
46
+
47
+ Thread-safe.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ name: str = "default",
53
+ failure_threshold: int = 5,
54
+ success_threshold: int = 2,
55
+ timeout_seconds: int = 60,
56
+ ):
57
+ """Initialize circuit breaker.
58
+
59
+ Args:
60
+ name: Name for logging
61
+ failure_threshold: Number of failures before opening circuit
62
+ success_threshold: Number of successes needed to close circuit (from half-open)
63
+ timeout_seconds: Seconds to wait before trying half-open
64
+ """
65
+ self.name = name
66
+ self.failure_threshold = failure_threshold
67
+ self.success_threshold = success_threshold
68
+ self.timeout_seconds = timeout_seconds
69
+
70
+ self._state = CircuitState.CLOSED
71
+ self._failure_count = 0
72
+ self._success_count = 0
73
+ self._last_failure_time: datetime | None = None
74
+ self._lock = threading.RLock()
75
+
76
+ @property
77
+ def state(self) -> CircuitState:
78
+ """Get current circuit state (thread-safe)."""
79
+ with self._lock:
80
+ return self._state
81
+
82
+ @property
83
+ def failure_count(self) -> int:
84
+ """Get current failure count (thread-safe)."""
85
+ with self._lock:
86
+ return self._failure_count
87
+
88
+ def call(self, func: Callable[..., T], *args, **kwargs) -> T:
89
+ """Execute a function with circuit breaker protection.
90
+
91
+ Args:
92
+ func: Function to call
93
+ *args: Positional arguments for func
94
+ **kwargs: Keyword arguments for func
95
+
96
+ Returns:
97
+ Return value from func
98
+
99
+ Raises:
100
+ CircuitBreakerError: If circuit is open
101
+ Exception: If func raises an exception
102
+ """
103
+ with self._lock:
104
+ current_state = self._state
105
+
106
+ # Check if we should transition from OPEN to HALF_OPEN
107
+ if current_state == CircuitState.OPEN:
108
+ if self._should_attempt_reset():
109
+ logger.info(
110
+ f"Circuit breaker '{self.name}' transitioning OPEN -> HALF_OPEN "
111
+ f"(timeout elapsed: {self.timeout_seconds}s)"
112
+ )
113
+ self._state = CircuitState.HALF_OPEN
114
+ self._success_count = 0
115
+ current_state = CircuitState.HALF_OPEN
116
+ else:
117
+ # Still open, reject immediately
118
+ time_since_failure = (
119
+ datetime.now() - self._last_failure_time
120
+ ).total_seconds()
121
+ raise CircuitBreakerError(
122
+ f"Circuit breaker '{self.name}' is OPEN "
123
+ f"({self._failure_count} failures, retry in "
124
+ f"{self.timeout_seconds - time_since_failure:.0f}s)"
125
+ )
126
+
127
+ # Execute the function (outside lock to avoid blocking other threads)
128
+ try:
129
+ result = func(*args, **kwargs)
130
+ self._on_success()
131
+ return result
132
+ except Exception as e:
133
+ self._on_failure(e)
134
+ raise
135
+
136
+ def _should_attempt_reset(self) -> bool:
137
+ """Check if enough time has passed to attempt reset (must hold lock)."""
138
+ if self._last_failure_time is None:
139
+ return True
140
+ elapsed = datetime.now() - self._last_failure_time
141
+ return elapsed >= timedelta(seconds=self.timeout_seconds)
142
+
143
+ def _on_success(self):
144
+ """Handle successful call (transitions HALF_OPEN -> CLOSED)."""
145
+ with self._lock:
146
+ if self._state == CircuitState.HALF_OPEN:
147
+ self._success_count += 1
148
+ logger.info(
149
+ f"Circuit breaker '{self.name}' success in HALF_OPEN "
150
+ f"({self._success_count}/{self.success_threshold})"
151
+ )
152
+
153
+ if self._success_count >= self.success_threshold:
154
+ logger.info(
155
+ f"Circuit breaker '{self.name}' transitioning HALF_OPEN -> CLOSED "
156
+ f"(service recovered)"
157
+ )
158
+ self._state = CircuitState.CLOSED
159
+ self._failure_count = 0
160
+ self._success_count = 0
161
+ self._last_failure_time = None
162
+
163
+ elif self._state == CircuitState.CLOSED:
164
+ # Reset failure count on success in closed state
165
+ if self._failure_count > 0:
166
+ logger.debug(
167
+ f"Circuit breaker '{self.name}' success, "
168
+ f"resetting failure count from {self._failure_count}"
169
+ )
170
+ self._failure_count = 0
171
+
172
+ def _on_failure(self, exception: Exception):
173
+ """Handle failed call (transitions CLOSED -> OPEN, HALF_OPEN -> OPEN)."""
174
+ with self._lock:
175
+ self._failure_count += 1
176
+ self._last_failure_time = datetime.now()
177
+
178
+ if self._state == CircuitState.HALF_OPEN:
179
+ # Any failure in half-open immediately opens circuit
180
+ logger.warning(
181
+ f"Circuit breaker '{self.name}' failed in HALF_OPEN -> OPEN "
182
+ f"(service still failing: {exception})"
183
+ )
184
+ self._state = CircuitState.OPEN
185
+ self._success_count = 0
186
+
187
+ elif self._state == CircuitState.CLOSED:
188
+ if self._failure_count >= self.failure_threshold:
189
+ logger.error(
190
+ f"Circuit breaker '{self.name}' CLOSED -> OPEN "
191
+ f"(threshold reached: {self._failure_count} failures)"
192
+ )
193
+ self._state = CircuitState.OPEN
194
+ else:
195
+ logger.warning(
196
+ f"Circuit breaker '{self.name}' failure "
197
+ f"{self._failure_count}/{self.failure_threshold}: {exception}"
198
+ )
199
+
200
+ def reset(self):
201
+ """Manually reset circuit breaker to CLOSED state."""
202
+ with self._lock:
203
+ logger.info(f"Circuit breaker '{self.name}' manually reset to CLOSED")
204
+ self._state = CircuitState.CLOSED
205
+ self._failure_count = 0
206
+ self._success_count = 0
207
+ self._last_failure_time = None
208
+
209
+ def get_status(self) -> dict:
210
+ """Get circuit breaker status (for monitoring/debugging)."""
211
+ with self._lock:
212
+ status = {
213
+ "name": self.name,
214
+ "state": self._state.value,
215
+ "failure_count": self._failure_count,
216
+ "success_count": self._success_count,
217
+ "failure_threshold": self.failure_threshold,
218
+ "success_threshold": self.success_threshold,
219
+ "timeout_seconds": self.timeout_seconds,
220
+ }
221
+
222
+ if self._last_failure_time:
223
+ status["last_failure_time"] = self._last_failure_time.isoformat()
224
+ time_since_failure = (
225
+ datetime.now() - self._last_failure_time
226
+ ).total_seconds()
227
+ status["seconds_since_failure"] = int(time_since_failure)
228
+
229
+ return status