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,87 @@
1
+ """Cursor AI IDE adapter."""
2
+
3
+ import asyncio
4
+ import os
5
+ import shutil
6
+
7
+ from app.executors.registry import ExecutorRegistry
8
+ from app.executors.spec import (
9
+ ExecutionRequest,
10
+ ExecutionResult,
11
+ ExecutorAdapter,
12
+ ExecutorCapability,
13
+ ExecutorInvocationError,
14
+ ExecutorMetadata,
15
+ ExecutorNotFoundError,
16
+ )
17
+
18
+
19
+ @ExecutorRegistry.register("cursor")
20
+ class CursorAdapter(ExecutorAdapter):
21
+ """Cursor AI IDE adapter."""
22
+
23
+ def get_metadata(self) -> ExecutorMetadata:
24
+ return ExecutorMetadata(
25
+ name="cursor",
26
+ display_name="Cursor",
27
+ version="1.0.0",
28
+ capabilities=[
29
+ ExecutorCapability.INTERACTIVE, # Opens IDE
30
+ ExecutorCapability.MCP_SERVERS,
31
+ ],
32
+ config_schema={
33
+ "type": "object",
34
+ "properties": {
35
+ "auto_apply": {
36
+ "type": "boolean",
37
+ "default": False,
38
+ "description": "Auto-apply suggestions without confirmation",
39
+ }
40
+ },
41
+ },
42
+ documentation_url="https://cursor.sh/",
43
+ author="Cursor",
44
+ license="Proprietary",
45
+ )
46
+
47
+ async def is_available(self) -> bool:
48
+ """Check if cursor is installed."""
49
+ return shutil.which("cursor") is not None
50
+
51
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
52
+ """Execute using Cursor.
53
+
54
+ Note: Cursor is primarily interactive, so this opens the IDE
55
+ and requires human interaction to complete the task.
56
+ """
57
+ if not await self.is_available():
58
+ raise ExecutorNotFoundError(
59
+ "Cursor not found. Install from https://cursor.sh/"
60
+ )
61
+
62
+ # Build command - opens Cursor in the working directory
63
+ cmd = ["cursor", request.working_directory]
64
+
65
+ # Note: Cursor doesn't have a headless mode for autonomous execution
66
+ # This will open the IDE and the human must complete the work
67
+
68
+ try:
69
+ await asyncio.create_subprocess_exec(
70
+ *cmd,
71
+ stdout=asyncio.subprocess.PIPE,
72
+ stderr=asyncio.subprocess.PIPE,
73
+ env={**os.environ, **request.environment},
74
+ )
75
+
76
+ # For Cursor, we just launch it and return immediately
77
+ # The actual work happens interactively
78
+
79
+ return ExecutionResult(
80
+ exit_code=0,
81
+ stdout=f"Opened Cursor in {request.working_directory}\\nPrompt: {request.prompt}",
82
+ stderr="",
83
+ metadata={"interactive": True, "requires_human": True},
84
+ )
85
+
86
+ except Exception as e:
87
+ raise ExecutorInvocationError(f"Cursor execution failed: {str(e)}")
@@ -0,0 +1,123 @@
1
+ """Droid 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("droid")
22
+ class DroidAdapter(ExecutorAdapter):
23
+ """Droid CLI adapter for automated code changes."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="droid",
28
+ display_name="Droid 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": "default",
40
+ "description": "Model to use for Droid",
41
+ }
42
+ },
43
+ },
44
+ documentation_url="https://github.com/anthropics/droid",
45
+ author="Anthropic",
46
+ license="MIT",
47
+ )
48
+
49
+ async def is_available(self) -> bool:
50
+ """Check if droid CLI is installed."""
51
+ return shutil.which("droid") is not None
52
+
53
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
54
+ """Execute using Droid CLI."""
55
+ if not await self.is_available():
56
+ raise ExecutorNotFoundError(
57
+ "Droid CLI not found. Install from https://github.com/anthropics/droid"
58
+ )
59
+
60
+ cmd = ["droid", "--print"]
61
+
62
+ if request.yolo_mode:
63
+ cmd.append("--dangerously-skip-permissions")
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"Droid execution timed out after {request.timeout_seconds}s"
91
+ ) from None
92
+ except Exception as e:
93
+ raise ExecutorInvocationError(f"Droid 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("Droid CLI not found")
99
+
100
+ cmd = ["droid", "--print"]
101
+ if request.yolo_mode:
102
+ cmd.append("--dangerously-skip-permissions")
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,132 @@
1
+ """Google Gemini 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("gemini")
22
+ class GeminiAdapter(ExecutorAdapter):
23
+ """Google Gemini CLI adapter."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="gemini",
28
+ display_name="Google Gemini 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": "gemini-2.0-flash",
40
+ "enum": ["gemini-2.0-flash", "gemini-pro", "gemini-ultra"],
41
+ "description": "Gemini model to use",
42
+ },
43
+ "sandbox": {
44
+ "type": "string",
45
+ "enum": ["docker", "local"],
46
+ "default": "docker",
47
+ "description": "Execution sandbox environment",
48
+ },
49
+ },
50
+ },
51
+ documentation_url="https://github.com/google/gemini-cli",
52
+ author="Google",
53
+ license="Apache-2.0",
54
+ )
55
+
56
+ async def is_available(self) -> bool:
57
+ """Check if gemini CLI is installed."""
58
+ return shutil.which("gemini") is not None
59
+
60
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
61
+ """Execute using Gemini CLI."""
62
+ if not await self.is_available():
63
+ raise ExecutorNotFoundError(
64
+ "Gemini CLI not found. Install from https://github.com/google/gemini-cli"
65
+ )
66
+
67
+ # Build command
68
+ cmd = ["gemini", "--print"]
69
+
70
+ if request.yolo_mode:
71
+ cmd.append("--yolo")
72
+
73
+ # Add model if specified
74
+ model = request.config.get("model", "gemini-2.0-flash")
75
+ cmd.extend(["--model", model])
76
+
77
+ # Add the prompt
78
+ cmd.extend(["--prompt", request.prompt])
79
+
80
+ try:
81
+ process = await asyncio.create_subprocess_exec(
82
+ *cmd,
83
+ cwd=request.working_directory,
84
+ stdout=asyncio.subprocess.PIPE,
85
+ stderr=asyncio.subprocess.PIPE,
86
+ env={**os.environ, **request.environment},
87
+ )
88
+
89
+ stdout, stderr = await asyncio.wait_for(
90
+ process.communicate(), timeout=request.timeout_seconds
91
+ )
92
+
93
+ return ExecutionResult(
94
+ exit_code=process.returncode,
95
+ stdout=stdout.decode("utf-8", errors="replace"),
96
+ stderr=stderr.decode("utf-8", errors="replace"),
97
+ duration_seconds=0.0,
98
+ )
99
+
100
+ except TimeoutError:
101
+ process.kill()
102
+ raise ExecutorTimeoutError(
103
+ f"Gemini execution timed out after {request.timeout_seconds}s"
104
+ )
105
+ except Exception as e:
106
+ raise ExecutorInvocationError(f"Gemini execution failed: {str(e)}")
107
+
108
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
109
+ """Stream output in real-time."""
110
+ if not await self.is_available():
111
+ raise ExecutorNotFoundError("Gemini CLI not found")
112
+
113
+ cmd = ["gemini", "--print"]
114
+ if request.yolo_mode:
115
+ cmd.append("--yolo")
116
+ cmd.extend(["--prompt", request.prompt])
117
+
118
+ process = await asyncio.create_subprocess_exec(
119
+ *cmd,
120
+ cwd=request.working_directory,
121
+ stdout=asyncio.subprocess.PIPE,
122
+ stderr=asyncio.subprocess.STDOUT,
123
+ env={**os.environ, **request.environment},
124
+ )
125
+
126
+ while True:
127
+ line = await process.stdout.readline()
128
+ if not line:
129
+ break
130
+ yield line.decode("utf-8", errors="replace")
131
+
132
+ await process.wait()
@@ -0,0 +1,131 @@
1
+ """Goose AI assistant 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("goose")
22
+ class GooseAdapter(ExecutorAdapter):
23
+ """Goose AI assistant adapter."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="goose",
28
+ display_name="Goose",
29
+ version="1.0.0",
30
+ capabilities=[
31
+ ExecutorCapability.STREAMING_OUTPUT,
32
+ ExecutorCapability.SESSION_RESUME,
33
+ ],
34
+ config_schema={
35
+ "type": "object",
36
+ "properties": {
37
+ "model": {
38
+ "type": "string",
39
+ "default": "gpt-4",
40
+ "description": "LLM model to use",
41
+ },
42
+ "profile": {
43
+ "type": "string",
44
+ "description": "Goose profile to use",
45
+ },
46
+ },
47
+ },
48
+ documentation_url="https://github.com/square/goose",
49
+ author="Square",
50
+ license="Apache-2.0",
51
+ )
52
+
53
+ async def is_available(self) -> bool:
54
+ """Check if goose is installed."""
55
+ return shutil.which("goose") is not None
56
+
57
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
58
+ """Execute using Goose."""
59
+ if not await self.is_available():
60
+ raise ExecutorNotFoundError(
61
+ "Goose not found. Install: pip install goose-ai"
62
+ )
63
+
64
+ # Build command
65
+ cmd = ["goose", "run"]
66
+
67
+ # Add session if provided
68
+ if request.session_id:
69
+ cmd.extend(["--session", request.session_id])
70
+
71
+ # Add profile if specified
72
+ profile = request.config.get("profile")
73
+ if profile:
74
+ cmd.extend(["--profile", profile])
75
+
76
+ # Add the prompt
77
+ cmd.append(request.prompt)
78
+
79
+ try:
80
+ process = await asyncio.create_subprocess_exec(
81
+ *cmd,
82
+ cwd=request.working_directory,
83
+ stdout=asyncio.subprocess.PIPE,
84
+ stderr=asyncio.subprocess.PIPE,
85
+ env={**os.environ, **request.environment},
86
+ )
87
+
88
+ stdout, stderr = await asyncio.wait_for(
89
+ process.communicate(), timeout=request.timeout_seconds
90
+ )
91
+
92
+ return ExecutionResult(
93
+ exit_code=process.returncode,
94
+ stdout=stdout.decode("utf-8", errors="replace"),
95
+ stderr=stderr.decode("utf-8", errors="replace"),
96
+ session_id=request.session_id, # Can be resumed
97
+ duration_seconds=0.0,
98
+ )
99
+
100
+ except TimeoutError:
101
+ process.kill()
102
+ raise ExecutorTimeoutError(
103
+ f"Goose execution timed out after {request.timeout_seconds}s"
104
+ )
105
+ except Exception as e:
106
+ raise ExecutorInvocationError(f"Goose execution failed: {str(e)}")
107
+
108
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
109
+ """Stream output in real-time."""
110
+ if not await self.is_available():
111
+ raise ExecutorNotFoundError("Goose not found")
112
+
113
+ cmd = ["goose", "run", request.prompt]
114
+ if request.session_id:
115
+ cmd.extend(["--session", request.session_id])
116
+
117
+ process = await asyncio.create_subprocess_exec(
118
+ *cmd,
119
+ cwd=request.working_directory,
120
+ stdout=asyncio.subprocess.PIPE,
121
+ stderr=asyncio.subprocess.STDOUT,
122
+ env={**os.environ, **request.environment},
123
+ )
124
+
125
+ while True:
126
+ line = await process.stdout.readline()
127
+ if not line:
128
+ break
129
+ yield line.decode("utf-8", errors="replace")
130
+
131
+ await process.wait()
@@ -0,0 +1,123 @@
1
+ """OpenCode 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("opencode")
22
+ class OpenCodeAdapter(ExecutorAdapter):
23
+ """OpenCode CLI adapter for automated code changes."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="opencode",
28
+ display_name="OpenCode 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
+ "provider": {
38
+ "type": "string",
39
+ "default": "anthropic",
40
+ "description": "LLM provider for OpenCode",
41
+ }
42
+ },
43
+ },
44
+ documentation_url="https://github.com/opencode-ai/opencode",
45
+ author="OpenCode",
46
+ license="MIT",
47
+ )
48
+
49
+ async def is_available(self) -> bool:
50
+ """Check if opencode CLI is installed."""
51
+ return shutil.which("opencode") is not None
52
+
53
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
54
+ """Execute using OpenCode CLI."""
55
+ if not await self.is_available():
56
+ raise ExecutorNotFoundError(
57
+ "OpenCode CLI not found. Install from https://github.com/opencode-ai/opencode"
58
+ )
59
+
60
+ cmd = ["opencode", "--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"OpenCode execution timed out after {request.timeout_seconds}s"
91
+ ) from None
92
+ except Exception as e:
93
+ raise ExecutorInvocationError(f"OpenCode 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("OpenCode CLI not found")
99
+
100
+ cmd = ["opencode", "--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()