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,177 @@
1
+ """Claude Code CLI executor 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("claude")
22
+ class ClaudeAdapter(ExecutorAdapter):
23
+ """Built-in Claude Code CLI adapter."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="claude",
28
+ display_name="Claude Code",
29
+ version="1.0.0",
30
+ capabilities=[
31
+ ExecutorCapability.STREAMING_OUTPUT,
32
+ ExecutorCapability.YOLO_MODE,
33
+ ExecutorCapability.MCP_SERVERS,
34
+ ExecutorCapability.COST_TRACKING,
35
+ ],
36
+ config_schema={
37
+ "type": "object",
38
+ "properties": {
39
+ "model": {
40
+ "type": "string",
41
+ "default": "claude-sonnet-4-5",
42
+ "description": "Claude model to use",
43
+ },
44
+ "mcp_config": {
45
+ "type": "string",
46
+ "description": "Path to MCP config file",
47
+ },
48
+ },
49
+ },
50
+ documentation_url="https://docs.anthropic.com/claude-code",
51
+ author="Anthropic",
52
+ license="Proprietary",
53
+ )
54
+
55
+ async def is_available(self) -> bool:
56
+ """Check if claude CLI is installed."""
57
+ return shutil.which("claude") is not None
58
+
59
+ async def check_availability(self) -> dict:
60
+ """Return detailed availability diagnostics."""
61
+ cli_path = shutil.which("claude")
62
+ issues = []
63
+ version = None
64
+
65
+ if not cli_path:
66
+ issues.append("Claude Code CLI not found in PATH")
67
+ else:
68
+ try:
69
+ proc = await asyncio.create_subprocess_exec(
70
+ "claude",
71
+ "--version",
72
+ stdout=asyncio.subprocess.PIPE,
73
+ stderr=asyncio.subprocess.PIPE,
74
+ )
75
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
76
+ version = stdout.decode().strip()
77
+ except Exception:
78
+ issues.append("Could not detect Claude Code version")
79
+
80
+ return {
81
+ "available": cli_path is not None,
82
+ "cli_found": cli_path is not None,
83
+ "cli_path": cli_path,
84
+ "version": version,
85
+ "issues": issues,
86
+ "setup_instructions": self.get_setup_instructions(),
87
+ }
88
+
89
+ def get_setup_instructions(self) -> str:
90
+ return (
91
+ "## Install Claude Code\n\n"
92
+ "```bash\n"
93
+ "npm install -g @anthropic-ai/claude-code\n"
94
+ "```\n\n"
95
+ "Then authenticate:\n"
96
+ "```bash\n"
97
+ "claude auth login\n"
98
+ "```\n\n"
99
+ "Docs: https://docs.anthropic.com/claude-code"
100
+ )
101
+
102
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
103
+ """Execute using Claude Code CLI."""
104
+ if not await self.is_available():
105
+ raise ExecutorNotFoundError(
106
+ "Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code"
107
+ )
108
+
109
+ # Build command
110
+ cmd = ["claude", "--print"]
111
+
112
+ if request.yolo_mode:
113
+ cmd.append("--dangerously-skip-permissions")
114
+
115
+ # Add MCP servers if configured
116
+ if request.mcp_servers:
117
+ for server in request.mcp_servers:
118
+ cmd.extend(["--mcp-server", server["name"]])
119
+
120
+ # Add the prompt
121
+ cmd.append(request.prompt)
122
+
123
+ # Execute
124
+ try:
125
+ process = await asyncio.create_subprocess_exec(
126
+ *cmd,
127
+ cwd=request.working_directory,
128
+ stdout=asyncio.subprocess.PIPE,
129
+ stderr=asyncio.subprocess.PIPE,
130
+ env={**os.environ, **request.environment},
131
+ )
132
+
133
+ stdout, stderr = await asyncio.wait_for(
134
+ process.communicate(), timeout=request.timeout_seconds
135
+ )
136
+
137
+ return ExecutionResult(
138
+ exit_code=process.returncode,
139
+ stdout=stdout.decode("utf-8", errors="replace"),
140
+ stderr=stderr.decode("utf-8", errors="replace"),
141
+ files_changed=[], # TODO: Parse from output
142
+ duration_seconds=0.0, # TODO: Track duration
143
+ )
144
+
145
+ except TimeoutError:
146
+ process.kill()
147
+ raise ExecutorTimeoutError(
148
+ f"Claude execution timed out after {request.timeout_seconds}s"
149
+ )
150
+ except Exception as e:
151
+ raise ExecutorInvocationError(f"Claude execution failed: {str(e)}")
152
+
153
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
154
+ """Stream output in real-time."""
155
+ if not await self.is_available():
156
+ raise ExecutorNotFoundError("Claude Code CLI not found")
157
+
158
+ cmd = ["claude", "--print"]
159
+ if request.yolo_mode:
160
+ cmd.append("--dangerously-skip-permissions")
161
+ cmd.append(request.prompt)
162
+
163
+ process = await asyncio.create_subprocess_exec(
164
+ *cmd,
165
+ cwd=request.working_directory,
166
+ stdout=asyncio.subprocess.PIPE,
167
+ stderr=asyncio.subprocess.STDOUT,
168
+ env={**os.environ, **request.environment},
169
+ )
170
+
171
+ while True:
172
+ line = await process.stdout.readline()
173
+ if not line:
174
+ break
175
+ yield line.decode("utf-8", errors="replace")
176
+
177
+ await process.wait()
@@ -0,0 +1,127 @@
1
+ """Cline 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("cline")
22
+ class ClineAdapter(ExecutorAdapter):
23
+ """Cline AI assistant adapter (VS Code extension CLI)."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="cline",
28
+ display_name="Cline",
29
+ version="1.0.0",
30
+ capabilities=[
31
+ ExecutorCapability.STREAMING_OUTPUT,
32
+ ],
33
+ config_schema={
34
+ "type": "object",
35
+ "properties": {
36
+ "model": {
37
+ "type": "string",
38
+ "default": "claude-3-5-sonnet-20241022",
39
+ "description": "LLM model to use",
40
+ },
41
+ "api_provider": {
42
+ "type": "string",
43
+ "enum": ["anthropic", "openai", "bedrock"],
44
+ "default": "anthropic",
45
+ "description": "API provider",
46
+ },
47
+ },
48
+ },
49
+ documentation_url="https://github.com/cline/cline",
50
+ author="Cline",
51
+ license="Apache-2.0",
52
+ )
53
+
54
+ async def is_available(self) -> bool:
55
+ """Check if cline CLI is installed."""
56
+ return shutil.which("cline") is not None
57
+
58
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
59
+ """Execute using Cline."""
60
+ if not await self.is_available():
61
+ raise ExecutorNotFoundError(
62
+ "Cline not found. Install the Cline VS Code extension with CLI support."
63
+ )
64
+
65
+ # Build command
66
+ cmd = ["cline", "execute"]
67
+
68
+ # Add model and provider
69
+ model = request.config.get("model", "claude-3-5-sonnet-20241022")
70
+ provider = request.config.get("api_provider", "anthropic")
71
+
72
+ cmd.extend(["--model", model])
73
+ cmd.extend(["--provider", provider])
74
+
75
+ # Add the prompt
76
+ cmd.extend(["--prompt", request.prompt])
77
+
78
+ try:
79
+ process = await asyncio.create_subprocess_exec(
80
+ *cmd,
81
+ cwd=request.working_directory,
82
+ stdout=asyncio.subprocess.PIPE,
83
+ stderr=asyncio.subprocess.PIPE,
84
+ env={**os.environ, **request.environment},
85
+ )
86
+
87
+ stdout, stderr = await asyncio.wait_for(
88
+ process.communicate(), timeout=request.timeout_seconds
89
+ )
90
+
91
+ return ExecutionResult(
92
+ exit_code=process.returncode,
93
+ stdout=stdout.decode("utf-8", errors="replace"),
94
+ stderr=stderr.decode("utf-8", errors="replace"),
95
+ duration_seconds=0.0,
96
+ )
97
+
98
+ except TimeoutError:
99
+ process.kill()
100
+ raise ExecutorTimeoutError(
101
+ f"Cline execution timed out after {request.timeout_seconds}s"
102
+ )
103
+ except Exception as e:
104
+ raise ExecutorInvocationError(f"Cline execution failed: {str(e)}")
105
+
106
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
107
+ """Stream output in real-time."""
108
+ if not await self.is_available():
109
+ raise ExecutorNotFoundError("Cline not found")
110
+
111
+ cmd = ["cline", "execute", "--prompt", request.prompt]
112
+
113
+ process = await asyncio.create_subprocess_exec(
114
+ *cmd,
115
+ cwd=request.working_directory,
116
+ stdout=asyncio.subprocess.PIPE,
117
+ stderr=asyncio.subprocess.STDOUT,
118
+ env={**os.environ, **request.environment},
119
+ )
120
+
121
+ while True:
122
+ line = await process.stdout.readline()
123
+ if not line:
124
+ break
125
+ yield line.decode("utf-8", errors="replace")
126
+
127
+ await process.wait()
@@ -0,0 +1,167 @@
1
+ """OpenAI Codex 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("codex")
22
+ class CodexAdapter(ExecutorAdapter):
23
+ """OpenAI Codex CLI adapter."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="codex",
28
+ display_name="OpenAI Codex 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": "o3",
40
+ "description": "OpenAI model used by Codex",
41
+ }
42
+ },
43
+ },
44
+ documentation_url="https://github.com/openai/codex",
45
+ author="OpenAI",
46
+ license="Apache-2.0",
47
+ )
48
+
49
+ async def is_available(self) -> bool:
50
+ """Check if codex CLI is installed."""
51
+ return shutil.which("codex") is not None
52
+
53
+ async def check_availability(self) -> dict:
54
+ """Return detailed availability diagnostics."""
55
+ cli_path = shutil.which("codex")
56
+ issues = []
57
+ version = None
58
+
59
+ if not cli_path:
60
+ issues.append("Codex CLI not found in PATH")
61
+ else:
62
+ try:
63
+ proc = await asyncio.create_subprocess_exec(
64
+ "codex",
65
+ "--version",
66
+ stdout=asyncio.subprocess.PIPE,
67
+ stderr=asyncio.subprocess.PIPE,
68
+ )
69
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
70
+ version = stdout.decode().strip()
71
+ except Exception:
72
+ issues.append("Could not detect Codex CLI version")
73
+
74
+ return {
75
+ "available": cli_path is not None,
76
+ "cli_found": cli_path is not None,
77
+ "cli_path": cli_path,
78
+ "version": version,
79
+ "issues": issues,
80
+ "setup_instructions": self.get_setup_instructions(),
81
+ }
82
+
83
+ def get_setup_instructions(self) -> str:
84
+ return (
85
+ "## Install OpenAI Codex CLI\n\n"
86
+ "```bash\n"
87
+ "npm install -g @openai/codex\n"
88
+ "```\n\n"
89
+ "Then authenticate:\n"
90
+ "```bash\n"
91
+ "export OPENAI_API_KEY=your-key\n"
92
+ "```\n\n"
93
+ "Docs: https://github.com/openai/codex"
94
+ )
95
+
96
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
97
+ """Execute using Codex CLI."""
98
+ if not await self.is_available():
99
+ raise ExecutorNotFoundError(
100
+ "Codex CLI not found. Install from https://github.com/openai/codex"
101
+ )
102
+
103
+ cmd = ["codex", "--print", "--auto-edit"]
104
+
105
+ if request.yolo_mode:
106
+ cmd.append("--full-auto")
107
+
108
+ try:
109
+ process = await asyncio.create_subprocess_exec(
110
+ *cmd,
111
+ cwd=request.working_directory,
112
+ stdin=asyncio.subprocess.PIPE,
113
+ stdout=asyncio.subprocess.PIPE,
114
+ stderr=asyncio.subprocess.PIPE,
115
+ env={**os.environ, **request.environment},
116
+ )
117
+
118
+ stdout, stderr = await asyncio.wait_for(
119
+ process.communicate(input=request.prompt.encode("utf-8")),
120
+ timeout=request.timeout_seconds,
121
+ )
122
+
123
+ return ExecutionResult(
124
+ exit_code=process.returncode,
125
+ stdout=stdout.decode("utf-8", errors="replace"),
126
+ stderr=stderr.decode("utf-8", errors="replace"),
127
+ duration_seconds=0.0,
128
+ )
129
+
130
+ except TimeoutError:
131
+ process.kill()
132
+ raise ExecutorTimeoutError(
133
+ f"Codex execution timed out after {request.timeout_seconds}s"
134
+ ) from None
135
+ except Exception as e:
136
+ raise ExecutorInvocationError(f"Codex execution failed: {e!s}") from e
137
+
138
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
139
+ """Stream output in real-time."""
140
+ if not await self.is_available():
141
+ raise ExecutorNotFoundError("Codex CLI not found")
142
+
143
+ cmd = ["codex", "--print", "--auto-edit"]
144
+ if request.yolo_mode:
145
+ cmd.append("--full-auto")
146
+
147
+ process = await asyncio.create_subprocess_exec(
148
+ *cmd,
149
+ cwd=request.working_directory,
150
+ stdin=asyncio.subprocess.PIPE,
151
+ stdout=asyncio.subprocess.PIPE,
152
+ stderr=asyncio.subprocess.STDOUT,
153
+ env={**os.environ, **request.environment},
154
+ )
155
+
156
+ # Send prompt via stdin then close
157
+ process.stdin.write(request.prompt.encode("utf-8"))
158
+ await process.stdin.drain()
159
+ process.stdin.close()
160
+
161
+ while True:
162
+ line = await process.stdout.readline()
163
+ if not line:
164
+ break
165
+ yield line.decode("utf-8", errors="replace")
166
+
167
+ await process.wait()
@@ -0,0 +1,202 @@
1
+ """GitHub Copilot 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("copilot")
22
+ class CopilotAdapter(ExecutorAdapter):
23
+ """GitHub Copilot CLI adapter."""
24
+
25
+ def get_metadata(self) -> ExecutorMetadata:
26
+ return ExecutorMetadata(
27
+ name="copilot",
28
+ display_name="GitHub Copilot CLI",
29
+ version="1.0.0",
30
+ capabilities=[
31
+ ExecutorCapability.STREAMING_OUTPUT,
32
+ ],
33
+ config_schema={
34
+ "type": "object",
35
+ "properties": {
36
+ "model": {
37
+ "type": "string",
38
+ "default": "gpt-4",
39
+ "description": "OpenAI model used by Copilot",
40
+ }
41
+ },
42
+ },
43
+ documentation_url="https://githubnext.com/projects/copilot-cli/",
44
+ author="GitHub",
45
+ license="Proprietary",
46
+ )
47
+
48
+ async def is_available(self) -> bool:
49
+ """Check if copilot CLI is installed."""
50
+ # GitHub Copilot CLI is accessed via `gh copilot` or dedicated `copilot` command
51
+ has_gh = shutil.which("gh") is not None
52
+ has_copilot = shutil.which("copilot") is not None
53
+
54
+ if has_gh:
55
+ # Check if copilot extension is installed
56
+ try:
57
+ process = await asyncio.create_subprocess_exec(
58
+ "gh",
59
+ "extension",
60
+ "list",
61
+ stdout=asyncio.subprocess.PIPE,
62
+ stderr=asyncio.subprocess.PIPE,
63
+ )
64
+ stdout, _ = await process.communicate()
65
+ return b"copilot" in stdout.lower()
66
+ except Exception:
67
+ return False
68
+
69
+ return has_copilot
70
+
71
+ async def check_availability(self) -> dict:
72
+ """Return detailed availability diagnostics."""
73
+ has_gh = shutil.which("gh")
74
+ has_copilot_cli = shutil.which("copilot")
75
+ issues = []
76
+ version = None
77
+ copilot_ext_installed = False
78
+
79
+ if not has_gh and not has_copilot_cli:
80
+ issues.append("Neither 'gh' CLI nor 'copilot' CLI found in PATH")
81
+ elif has_gh:
82
+ try:
83
+ proc = await asyncio.create_subprocess_exec(
84
+ "gh",
85
+ "extension",
86
+ "list",
87
+ stdout=asyncio.subprocess.PIPE,
88
+ stderr=asyncio.subprocess.PIPE,
89
+ )
90
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
91
+ if b"copilot" in stdout.lower():
92
+ copilot_ext_installed = True
93
+ else:
94
+ issues.append("GitHub Copilot extension not installed in gh CLI")
95
+ except Exception:
96
+ issues.append("Could not check gh extensions")
97
+
98
+ try:
99
+ proc = await asyncio.create_subprocess_exec(
100
+ "gh",
101
+ "--version",
102
+ stdout=asyncio.subprocess.PIPE,
103
+ stderr=asyncio.subprocess.PIPE,
104
+ )
105
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
106
+ version = stdout.decode().strip().split("\n")[0]
107
+ except Exception:
108
+ pass
109
+
110
+ return {
111
+ "available": copilot_ext_installed or has_copilot_cli is not None,
112
+ "cli_found": has_gh is not None or has_copilot_cli is not None,
113
+ "cli_path": has_gh or has_copilot_cli,
114
+ "version": version,
115
+ "copilot_extension_installed": copilot_ext_installed,
116
+ "issues": issues,
117
+ "setup_instructions": self.get_setup_instructions(),
118
+ }
119
+
120
+ def get_setup_instructions(self) -> str:
121
+ return (
122
+ "## Install GitHub Copilot CLI\n\n"
123
+ "1. Install the GitHub CLI:\n"
124
+ "```bash\n"
125
+ "brew install gh # macOS\n"
126
+ "```\n\n"
127
+ "2. Install the Copilot extension:\n"
128
+ "```bash\n"
129
+ "gh extension install github/gh-copilot\n"
130
+ "```\n\n"
131
+ "Docs: https://githubnext.com/projects/copilot-cli/"
132
+ )
133
+
134
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
135
+ """Execute using GitHub Copilot CLI."""
136
+ if not await self.is_available():
137
+ raise ExecutorNotFoundError(
138
+ "GitHub Copilot CLI not found. Install: gh extension install github/gh-copilot"
139
+ )
140
+
141
+ # Determine command structure
142
+ if shutil.which("gh"):
143
+ cmd = ["gh", "copilot", "suggest"]
144
+ else:
145
+ cmd = ["copilot", "suggest"]
146
+
147
+ # Add the prompt
148
+ cmd.append(request.prompt)
149
+
150
+ try:
151
+ process = await asyncio.create_subprocess_exec(
152
+ *cmd,
153
+ cwd=request.working_directory,
154
+ stdout=asyncio.subprocess.PIPE,
155
+ stderr=asyncio.subprocess.PIPE,
156
+ env={**os.environ, **request.environment},
157
+ )
158
+
159
+ stdout, stderr = await asyncio.wait_for(
160
+ process.communicate(), timeout=request.timeout_seconds
161
+ )
162
+
163
+ return ExecutionResult(
164
+ exit_code=process.returncode,
165
+ stdout=stdout.decode("utf-8", errors="replace"),
166
+ stderr=stderr.decode("utf-8", errors="replace"),
167
+ duration_seconds=0.0,
168
+ )
169
+
170
+ except TimeoutError:
171
+ process.kill()
172
+ raise ExecutorTimeoutError(
173
+ f"Copilot execution timed out after {request.timeout_seconds}s"
174
+ ) from None
175
+ except Exception as e:
176
+ raise ExecutorInvocationError(f"Copilot execution failed: {str(e)}") from e
177
+
178
+ async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
179
+ """Stream output in real-time."""
180
+ if not await self.is_available():
181
+ raise ExecutorNotFoundError("GitHub Copilot CLI not found")
182
+
183
+ if shutil.which("gh"):
184
+ cmd = ["gh", "copilot", "suggest", request.prompt]
185
+ else:
186
+ cmd = ["copilot", "suggest", request.prompt]
187
+
188
+ process = await asyncio.create_subprocess_exec(
189
+ *cmd,
190
+ cwd=request.working_directory,
191
+ stdout=asyncio.subprocess.PIPE,
192
+ stderr=asyncio.subprocess.STDOUT,
193
+ env={**os.environ, **request.environment},
194
+ )
195
+
196
+ while True:
197
+ line = await process.stdout.readline()
198
+ if not line:
199
+ break
200
+ yield line.decode("utf-8", errors="replace")
201
+
202
+ await process.wait()