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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- 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,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
|