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,123 @@
|
|
|
1
|
+
"""Qwen CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from app.executors.registry import ExecutorRegistry
|
|
9
|
+
from app.executors.spec import (
|
|
10
|
+
ExecutionRequest,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
ExecutorAdapter,
|
|
13
|
+
ExecutorCapability,
|
|
14
|
+
ExecutorInvocationError,
|
|
15
|
+
ExecutorMetadata,
|
|
16
|
+
ExecutorNotFoundError,
|
|
17
|
+
ExecutorTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ExecutorRegistry.register("qwen")
|
|
22
|
+
class QwenAdapter(ExecutorAdapter):
|
|
23
|
+
"""Qwen Code CLI adapter for automated code changes."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="qwen",
|
|
28
|
+
display_name="Qwen Code CLI",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.YOLO_MODE,
|
|
33
|
+
],
|
|
34
|
+
config_schema={
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"model": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"default": "qwen-coder",
|
|
40
|
+
"description": "Qwen model to use",
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
documentation_url="https://github.com/QwenLM/qwen-agent",
|
|
45
|
+
author="Alibaba Cloud",
|
|
46
|
+
license="Apache-2.0",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def is_available(self) -> bool:
|
|
50
|
+
"""Check if qwen CLI is installed."""
|
|
51
|
+
return shutil.which("qwen") is not None
|
|
52
|
+
|
|
53
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
54
|
+
"""Execute using Qwen CLI."""
|
|
55
|
+
if not await self.is_available():
|
|
56
|
+
raise ExecutorNotFoundError(
|
|
57
|
+
"Qwen CLI not found. Install from https://github.com/QwenLM/qwen-agent"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cmd = ["qwen", "--print"]
|
|
61
|
+
|
|
62
|
+
if request.yolo_mode:
|
|
63
|
+
cmd.append("--yolo")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
process = await asyncio.create_subprocess_exec(
|
|
67
|
+
*cmd,
|
|
68
|
+
cwd=request.working_directory,
|
|
69
|
+
stdin=asyncio.subprocess.PIPE,
|
|
70
|
+
stdout=asyncio.subprocess.PIPE,
|
|
71
|
+
stderr=asyncio.subprocess.PIPE,
|
|
72
|
+
env={**os.environ, **request.environment},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
stdout, stderr = await asyncio.wait_for(
|
|
76
|
+
process.communicate(input=request.prompt.encode("utf-8")),
|
|
77
|
+
timeout=request.timeout_seconds,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return ExecutionResult(
|
|
81
|
+
exit_code=process.returncode,
|
|
82
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
83
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
84
|
+
duration_seconds=0.0,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except TimeoutError:
|
|
88
|
+
process.kill()
|
|
89
|
+
raise ExecutorTimeoutError(
|
|
90
|
+
f"Qwen execution timed out after {request.timeout_seconds}s"
|
|
91
|
+
) from None
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ExecutorInvocationError(f"Qwen execution failed: {e!s}") from e
|
|
94
|
+
|
|
95
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
96
|
+
"""Stream output in real-time."""
|
|
97
|
+
if not await self.is_available():
|
|
98
|
+
raise ExecutorNotFoundError("Qwen CLI not found")
|
|
99
|
+
|
|
100
|
+
cmd = ["qwen", "--print"]
|
|
101
|
+
if request.yolo_mode:
|
|
102
|
+
cmd.append("--yolo")
|
|
103
|
+
|
|
104
|
+
process = await asyncio.create_subprocess_exec(
|
|
105
|
+
*cmd,
|
|
106
|
+
cwd=request.working_directory,
|
|
107
|
+
stdin=asyncio.subprocess.PIPE,
|
|
108
|
+
stdout=asyncio.subprocess.PIPE,
|
|
109
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
110
|
+
env={**os.environ, **request.environment},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
process.stdin.write(request.prompt.encode("utf-8"))
|
|
114
|
+
await process.stdin.drain()
|
|
115
|
+
process.stdin.close()
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
line = await process.stdout.readline()
|
|
119
|
+
if not line:
|
|
120
|
+
break
|
|
121
|
+
yield line.decode("utf-8", errors="replace")
|
|
122
|
+
|
|
123
|
+
await process.wait()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Community/third-party executor plugins."""
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Executor plugin registry with dynamic loading."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import pkgutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from app.executors.spec import ExecutorAdapter, ExecutorMetadata, ExecutorNotFoundError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExecutorRegistry:
|
|
14
|
+
"""Registry for executor plugins with dynamic loading."""
|
|
15
|
+
|
|
16
|
+
_adapters: dict[str, type[ExecutorAdapter]] = {}
|
|
17
|
+
_instances: dict[str, ExecutorAdapter] = {}
|
|
18
|
+
_loaded: bool = False
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def register(cls, name: str):
|
|
22
|
+
"""Decorator to register an executor adapter.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
@ExecutorRegistry.register("claude")
|
|
26
|
+
class ClaudeAdapter(ExecutorAdapter):
|
|
27
|
+
...
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def decorator(adapter_class: type[ExecutorAdapter]):
|
|
31
|
+
if name in cls._adapters:
|
|
32
|
+
logger.warning(f"Overwriting existing executor adapter: {name}")
|
|
33
|
+
cls._adapters[name] = adapter_class
|
|
34
|
+
logger.info(f"Registered executor adapter: {name}")
|
|
35
|
+
return adapter_class
|
|
36
|
+
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get(cls, name: str) -> ExecutorAdapter:
|
|
41
|
+
"""Get an executor adapter instance by name.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: Executor name (e.g., "claude", "cursor")
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ExecutorAdapter instance
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ExecutorNotFoundError: If executor not registered
|
|
51
|
+
"""
|
|
52
|
+
# Ensure plugins are loaded
|
|
53
|
+
if not cls._loaded:
|
|
54
|
+
cls.load_all_plugins()
|
|
55
|
+
|
|
56
|
+
if name not in cls._adapters:
|
|
57
|
+
raise ExecutorNotFoundError(
|
|
58
|
+
f"Unknown executor: {name}. Available: {list(cls._adapters.keys())}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Return cached instance or create new
|
|
62
|
+
if name not in cls._instances:
|
|
63
|
+
cls._instances[name] = cls._adapters[name]()
|
|
64
|
+
|
|
65
|
+
return cls._instances[name]
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
async def get_available(cls) -> list[ExecutorMetadata]:
|
|
69
|
+
"""List all available executors (installed and accessible).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of ExecutorMetadata for available executors
|
|
73
|
+
"""
|
|
74
|
+
if not cls._loaded:
|
|
75
|
+
cls.load_all_plugins()
|
|
76
|
+
|
|
77
|
+
available = []
|
|
78
|
+
for name, _adapter_class in cls._adapters.items():
|
|
79
|
+
try:
|
|
80
|
+
adapter = cls.get(name)
|
|
81
|
+
if await adapter.is_available():
|
|
82
|
+
available.append(adapter.get_metadata())
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Failed to check availability of {name}: {e}")
|
|
85
|
+
|
|
86
|
+
return available
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def list_all(cls) -> list[ExecutorMetadata]:
|
|
90
|
+
"""List all registered executors (may not be installed).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of ExecutorMetadata for all registered executors
|
|
94
|
+
"""
|
|
95
|
+
if not cls._loaded:
|
|
96
|
+
cls.load_all_plugins()
|
|
97
|
+
|
|
98
|
+
all_executors = []
|
|
99
|
+
for name, _adapter_class in cls._adapters.items():
|
|
100
|
+
try:
|
|
101
|
+
adapter = cls.get(name)
|
|
102
|
+
all_executors.append(adapter.get_metadata())
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.warning(f"Failed to get metadata for {name}: {e}")
|
|
105
|
+
|
|
106
|
+
return all_executors
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def load_all_plugins(cls):
|
|
110
|
+
"""Load all executor plugins from adapters and plugins directories."""
|
|
111
|
+
if cls._loaded:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
logger.info("Loading executor plugins...")
|
|
115
|
+
|
|
116
|
+
# Load built-in adapters
|
|
117
|
+
cls._load_plugins_from_package("app.executors.adapters")
|
|
118
|
+
|
|
119
|
+
# Load community plugins
|
|
120
|
+
cls._load_plugins_from_package("app.executors.plugins")
|
|
121
|
+
|
|
122
|
+
cls._loaded = True
|
|
123
|
+
logger.info(f"Loaded {len(cls._adapters)} executor plugins")
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _load_plugins_from_package(cls, package_name: str):
|
|
127
|
+
"""Load all Python modules from a package.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
package_name: Package name (e.g., "app.executors.adapters")
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
package = importlib.import_module(package_name)
|
|
134
|
+
package_path = Path(package.__file__).parent
|
|
135
|
+
|
|
136
|
+
for _finder, name, _ispkg in pkgutil.iter_modules([str(package_path)]):
|
|
137
|
+
if name.startswith("_"):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
module_name = f"{package_name}.{name}"
|
|
141
|
+
try:
|
|
142
|
+
importlib.import_module(module_name)
|
|
143
|
+
logger.debug(f"Loaded plugin module: {module_name}")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to load plugin {module_name}: {e}")
|
|
146
|
+
|
|
147
|
+
except ImportError as e:
|
|
148
|
+
logger.warning(f"Package {package_name} not found: {e}")
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def reload_plugins(cls):
|
|
152
|
+
"""Reload all plugins (useful for development)."""
|
|
153
|
+
cls._adapters.clear()
|
|
154
|
+
cls._instances.clear()
|
|
155
|
+
cls._loaded = False
|
|
156
|
+
cls.load_all_plugins()
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def unregister(cls, name: str):
|
|
160
|
+
"""Unregister an executor adapter.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
name: Executor name to unregister
|
|
164
|
+
"""
|
|
165
|
+
if name in cls._adapters:
|
|
166
|
+
del cls._adapters[name]
|
|
167
|
+
if name in cls._instances:
|
|
168
|
+
del cls._instances[name]
|
|
169
|
+
logger.info(f"Unregistered executor adapter: {name}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Convenience functions
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_executor(name: str) -> ExecutorAdapter:
|
|
176
|
+
"""Get an executor adapter by name.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
name: Executor name
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
ExecutorAdapter instance
|
|
183
|
+
"""
|
|
184
|
+
return ExecutorRegistry.get(name)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def list_available_executors() -> list[ExecutorMetadata]:
|
|
188
|
+
"""List all available executors.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of ExecutorMetadata
|
|
192
|
+
"""
|
|
193
|
+
return await ExecutorRegistry.get_available()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def list_all_executors() -> list[ExecutorMetadata]:
|
|
197
|
+
"""List all registered executors.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of ExecutorMetadata
|
|
201
|
+
"""
|
|
202
|
+
return ExecutorRegistry.list_all()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Draft Executor Adapter Specification v1.0
|
|
3
|
+
|
|
4
|
+
This defines the interface for adding new AI coding agents to Draft.
|
|
5
|
+
Implement this interface to create a new executor plugin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExecutorCapability(StrEnum):
|
|
15
|
+
"""Capabilities an executor may support."""
|
|
16
|
+
|
|
17
|
+
STREAMING_OUTPUT = "streaming_output" # Real-time stdout
|
|
18
|
+
SESSION_RESUME = "session_resume" # Continue previous sessions
|
|
19
|
+
YOLO_MODE = "yolo_mode" # Auto-approve all actions
|
|
20
|
+
MCP_SERVERS = "mcp_servers" # Model Context Protocol
|
|
21
|
+
COST_TRACKING = "cost_tracking" # Token/cost reporting
|
|
22
|
+
INTERACTIVE = "interactive" # Requires human interaction
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ExecutorMetadata:
|
|
27
|
+
"""Metadata about an executor."""
|
|
28
|
+
|
|
29
|
+
name: str # e.g., "claude", "codex"
|
|
30
|
+
display_name: str # e.g., "Claude Code", "OpenAI Codex"
|
|
31
|
+
version: str # Executor adapter version
|
|
32
|
+
agent_version: str | None = None # Underlying agent version if detectable
|
|
33
|
+
capabilities: list[ExecutorCapability] = field(default_factory=list)
|
|
34
|
+
config_schema: dict = field(default_factory=dict) # JSON Schema for configuration
|
|
35
|
+
documentation_url: str | None = None
|
|
36
|
+
author: str | None = None # Plugin author
|
|
37
|
+
license: str | None = None # Plugin license
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ExecutionRequest:
|
|
42
|
+
"""Request to execute a task."""
|
|
43
|
+
|
|
44
|
+
prompt: str
|
|
45
|
+
working_directory: str
|
|
46
|
+
timeout_seconds: int = 600
|
|
47
|
+
yolo_mode: bool = False
|
|
48
|
+
session_id: str | None = None # For session resume
|
|
49
|
+
environment: dict[str, str] = field(default_factory=dict) # Additional env vars
|
|
50
|
+
mcp_servers: list[dict] = field(default_factory=list) # MCP server configs
|
|
51
|
+
config: dict[str, any] = field(default_factory=dict) # Executor-specific config
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ExecutionResult:
|
|
56
|
+
"""Result of an execution."""
|
|
57
|
+
|
|
58
|
+
exit_code: int
|
|
59
|
+
stdout: str
|
|
60
|
+
stderr: str
|
|
61
|
+
session_id: str | None = None # For future resume
|
|
62
|
+
files_changed: list[str] = field(default_factory=list)
|
|
63
|
+
cost_usd: float | None = None
|
|
64
|
+
tokens_used: dict[str, int] | None = None # {"input": X, "output": Y}
|
|
65
|
+
duration_seconds: float = 0.0
|
|
66
|
+
metadata: dict[str, any] = field(default_factory=dict) # Executor-specific data
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ExecutorAdapter(ABC):
|
|
70
|
+
"""
|
|
71
|
+
Abstract base class for executor adapters.
|
|
72
|
+
|
|
73
|
+
Implement this to add a new AI coding agent to Draft.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
78
|
+
"""Return metadata about this executor.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
ExecutorMetadata with name, capabilities, etc.
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def is_available(self) -> bool:
|
|
87
|
+
"""Check if the underlying agent is installed and accessible.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if executor can be used, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
96
|
+
"""Execute a task and return the result.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
request: ExecutionRequest with prompt, working directory, etc.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ExecutionResult with exit code, stdout, stderr, etc.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ExecutorError: If execution fails
|
|
106
|
+
"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
110
|
+
"""
|
|
111
|
+
Stream output in real-time. Optional - implement if your agent supports it.
|
|
112
|
+
|
|
113
|
+
Default implementation runs execute() and yields all output at once.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
request: ExecutionRequest
|
|
117
|
+
|
|
118
|
+
Yields:
|
|
119
|
+
str: Output lines as they're produced
|
|
120
|
+
"""
|
|
121
|
+
result = await self.execute(request)
|
|
122
|
+
yield result.stdout
|
|
123
|
+
if result.stderr:
|
|
124
|
+
yield result.stderr
|
|
125
|
+
|
|
126
|
+
def get_mcp_config_path(self) -> str | None:
|
|
127
|
+
"""Return the path to this agent's MCP config file, if applicable.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Path to MCP config or None
|
|
131
|
+
"""
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def supports_capability(self, capability: ExecutorCapability) -> bool:
|
|
135
|
+
"""Check if this executor supports a capability.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
capability: ExecutorCapability to check
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if supported
|
|
142
|
+
"""
|
|
143
|
+
metadata = self.get_metadata()
|
|
144
|
+
return capability in metadata.capabilities
|
|
145
|
+
|
|
146
|
+
async def check_availability(self) -> dict[str, any]:
|
|
147
|
+
"""Return detailed availability diagnostics.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dict with keys:
|
|
151
|
+
- available (bool): Whether executor is ready
|
|
152
|
+
- cli_found (bool): Whether CLI binary was found
|
|
153
|
+
- version (str|None): Detected CLI version
|
|
154
|
+
- issues (list[str]): Problems preventing use
|
|
155
|
+
- setup_instructions (str): How to install/configure
|
|
156
|
+
|
|
157
|
+
Default implementation delegates to is_available().
|
|
158
|
+
Override for richer diagnostics.
|
|
159
|
+
"""
|
|
160
|
+
available = await self.is_available()
|
|
161
|
+
metadata = self.get_metadata()
|
|
162
|
+
return {
|
|
163
|
+
"available": available,
|
|
164
|
+
"cli_found": available,
|
|
165
|
+
"version": metadata.agent_version,
|
|
166
|
+
"issues": [] if available else ["CLI not found in PATH"],
|
|
167
|
+
"setup_instructions": self.get_setup_instructions(),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def get_setup_instructions(self) -> str:
|
|
171
|
+
"""Return human-readable setup instructions for this executor.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Markdown-formatted setup guide.
|
|
175
|
+
"""
|
|
176
|
+
metadata = self.get_metadata()
|
|
177
|
+
url = metadata.documentation_url or ""
|
|
178
|
+
return (
|
|
179
|
+
f"Install {metadata.display_name}. See {url}"
|
|
180
|
+
if url
|
|
181
|
+
else f"Install {metadata.display_name}."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def validate_config(self, config: dict) -> bool:
|
|
185
|
+
"""Validate executor-specific configuration.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
config: Configuration dict to validate
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if valid
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If config is invalid
|
|
195
|
+
"""
|
|
196
|
+
# Default: no validation
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ExecutorError(Exception):
|
|
201
|
+
"""Base exception for executor errors."""
|
|
202
|
+
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ExecutorNotFoundError(ExecutorError):
|
|
207
|
+
"""Raised when executor CLI is not found."""
|
|
208
|
+
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ExecutorInvocationError(ExecutorError):
|
|
213
|
+
"""Raised when executor CLI invocation fails."""
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self, message: str, exit_code: int | None = None, stderr: str | None = None
|
|
217
|
+
):
|
|
218
|
+
super().__init__(message)
|
|
219
|
+
self.exit_code = exit_code
|
|
220
|
+
self.stderr = stderr
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ExecutorTimeoutError(ExecutorError):
|
|
224
|
+
"""Raised when executor execution times out."""
|
|
225
|
+
|
|
226
|
+
pass
|