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,80 @@
|
|
|
1
|
+
"""Auth dependency: extract current user from JWT Bearer token.
|
|
2
|
+
|
|
3
|
+
When AUTH_ENABLED=false (the default for existing single-user setups),
|
|
4
|
+
all auth dependencies return None so existing routes keep working unchanged.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from fastapi import Depends, HTTPException, status
|
|
10
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from app.database import get_db
|
|
14
|
+
from app.models.user import User
|
|
15
|
+
from app.services.auth_service import decode_access_token, get_user_by_id
|
|
16
|
+
|
|
17
|
+
AUTH_ENABLED = os.getenv("AUTH_ENABLED", "false").lower() in ("true", "1", "yes")
|
|
18
|
+
|
|
19
|
+
# auto_error=False so the dependency doesn't 403 when no header is present
|
|
20
|
+
_bearer_scheme = HTTPBearer(auto_error=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def get_current_user(
|
|
24
|
+
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
|
|
25
|
+
db: AsyncSession = Depends(get_db),
|
|
26
|
+
) -> User | None:
|
|
27
|
+
"""Return the authenticated User, or None when auth is disabled.
|
|
28
|
+
|
|
29
|
+
When AUTH_ENABLED=true:
|
|
30
|
+
- Missing/invalid token -> 401
|
|
31
|
+
- Valid token -> User object
|
|
32
|
+
|
|
33
|
+
When AUTH_ENABLED=false:
|
|
34
|
+
- Always returns None (backward compatible, no login needed)
|
|
35
|
+
"""
|
|
36
|
+
if not AUTH_ENABLED:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if credentials is None:
|
|
40
|
+
raise HTTPException(
|
|
41
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
42
|
+
detail="Authentication required",
|
|
43
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
payload = decode_access_token(credentials.credentials)
|
|
47
|
+
if payload is None:
|
|
48
|
+
raise HTTPException(
|
|
49
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
50
|
+
detail="Invalid or expired token",
|
|
51
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
user = await get_user_by_id(db, payload["sub"])
|
|
55
|
+
if user is None or not user.is_active:
|
|
56
|
+
raise HTTPException(
|
|
57
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
58
|
+
detail="User not found or inactive",
|
|
59
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return user
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def get_optional_user(
|
|
66
|
+
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
|
|
67
|
+
db: AsyncSession = Depends(get_db),
|
|
68
|
+
) -> User | None:
|
|
69
|
+
"""Like get_current_user but never raises 401 — always returns User or None."""
|
|
70
|
+
if credentials is None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
payload = decode_access_token(credentials.credentials)
|
|
74
|
+
if payload is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
user = await get_user_by_id(db, payload["sub"])
|
|
78
|
+
if user is None or not user.is_active:
|
|
79
|
+
return None
|
|
80
|
+
return user
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Reusable FastAPI dependencies for validation and DI."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import Path
|
|
6
|
+
|
|
7
|
+
from app.utils.validators import validate_uuid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ValidatedUUID(field_name: str):
|
|
11
|
+
"""Create an annotated type for UUID path parameter validation.
|
|
12
|
+
|
|
13
|
+
Creates a type annotation that validates UUID format and returns the normalized UUID string.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
@router.get("/{ticket_id}")
|
|
17
|
+
async def get_ticket(
|
|
18
|
+
ticket_id: Annotated[str, ValidatedUUID("ticket_id")],
|
|
19
|
+
db: AsyncSession = Depends(get_db),
|
|
20
|
+
):
|
|
21
|
+
# ticket_id is guaranteed to be a valid UUID
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
field_name: Name of the field for error messages (e.g., "ticket_id")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A Path dependency that validates and returns the UUID
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def validator(value: str) -> str:
|
|
31
|
+
return validate_uuid(value, field_name)
|
|
32
|
+
|
|
33
|
+
return Path(..., description=f"Valid UUID for {field_name}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Type aliases for common ID types - use with Annotated
|
|
37
|
+
# Example: ticket_id: TicketID
|
|
38
|
+
TicketID = Annotated[str, Path(..., description="Valid UUID for ticket_id")]
|
|
39
|
+
JobID = Annotated[str, Path(..., description="Valid UUID for job_id")]
|
|
40
|
+
GoalID = Annotated[str, Path(..., description="Valid UUID for goal_id")]
|
|
41
|
+
BoardID = Annotated[str, Path(..., description="Valid UUID for board_id")]
|
|
42
|
+
RevisionID = Annotated[str, Path(..., description="Valid UUID for revision_id")]
|
|
43
|
+
EvidenceID = Annotated[str, Path(..., description="Valid UUID for evidence_id")]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Custom exceptions for Draft."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DraftError(Exception):
|
|
5
|
+
"""Base exception for Draft."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidStateTransitionError(DraftError):
|
|
11
|
+
"""Raised when an invalid state transition is attempted."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, from_state: str, to_state: str, message: str | None = None):
|
|
14
|
+
self.from_state = from_state
|
|
15
|
+
self.to_state = to_state
|
|
16
|
+
self.message = (
|
|
17
|
+
message or f"Invalid transition from '{from_state}' to '{to_state}'"
|
|
18
|
+
)
|
|
19
|
+
super().__init__(self.message)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResourceNotFoundError(DraftError):
|
|
23
|
+
"""Raised when a requested resource is not found."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, resource_type: str, resource_id: str):
|
|
26
|
+
self.resource_type = resource_type
|
|
27
|
+
self.resource_id = resource_id
|
|
28
|
+
self.message = f"{resource_type} with id '{resource_id}' not found"
|
|
29
|
+
super().__init__(self.message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidationError(DraftError):
|
|
33
|
+
"""Raised when validation fails."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str):
|
|
36
|
+
self.message = message
|
|
37
|
+
super().__init__(self.message)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ConflictError(DraftError):
|
|
41
|
+
"""Raised when an operation conflicts with current resource state.
|
|
42
|
+
|
|
43
|
+
Typically maps to HTTP 409 Conflict.
|
|
44
|
+
Example: Attempting to comment on a superseded revision.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, message: str):
|
|
48
|
+
self.message = message
|
|
49
|
+
super().__init__(self.message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WorkspaceError(DraftError):
|
|
53
|
+
"""Base exception for workspace-related errors."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, message: str):
|
|
56
|
+
self.message = message
|
|
57
|
+
super().__init__(self.message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NotAGitRepositoryError(WorkspaceError):
|
|
61
|
+
"""Raised when the configured path is not a git repository."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, path: str):
|
|
64
|
+
self.path = path
|
|
65
|
+
super().__init__(f"Repository at '{path}' is not a git repository")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WorktreeCreationError(WorkspaceError):
|
|
69
|
+
"""Raised when worktree creation fails."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str, git_error: str | None = None):
|
|
72
|
+
self.git_error = git_error
|
|
73
|
+
full_message = message
|
|
74
|
+
if git_error:
|
|
75
|
+
full_message = f"{message}: {git_error}"
|
|
76
|
+
super().__init__(full_message)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BranchNotFoundError(WorkspaceError):
|
|
80
|
+
"""Raised when the base branch is not found."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, branch: str):
|
|
83
|
+
self.branch = branch
|
|
84
|
+
super().__init__(f"Base branch '{branch}' not found in repository")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ExecutorError(DraftError):
|
|
88
|
+
"""Base exception for executor-related errors."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str):
|
|
91
|
+
self.message = message
|
|
92
|
+
super().__init__(self.message)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ExecutorNotFoundError(ExecutorError):
|
|
96
|
+
"""Raised when no supported code executor CLI is found."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, message: str | None = None):
|
|
99
|
+
default_message = (
|
|
100
|
+
"No supported code executor CLI found. "
|
|
101
|
+
"Please install Cursor CLI or Claude Code CLI."
|
|
102
|
+
)
|
|
103
|
+
super().__init__(message or default_message)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ExecutorInvocationError(ExecutorError):
|
|
107
|
+
"""Raised when the executor CLI invocation fails."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
message: str,
|
|
112
|
+
exit_code: int | None = None,
|
|
113
|
+
stderr: str | None = None,
|
|
114
|
+
):
|
|
115
|
+
self.exit_code = exit_code
|
|
116
|
+
self.stderr = stderr
|
|
117
|
+
full_message = message
|
|
118
|
+
if exit_code is not None:
|
|
119
|
+
full_message = f"{message} (exit code: {exit_code})"
|
|
120
|
+
if stderr:
|
|
121
|
+
full_message = f"{full_message}\nError output: {stderr}"
|
|
122
|
+
super().__init__(full_message)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ConfigurationError(DraftError):
|
|
126
|
+
"""Raised when required configuration is missing or invalid."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, message: str):
|
|
129
|
+
self.message = message
|
|
130
|
+
super().__init__(self.message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PlannerError(DraftError):
|
|
134
|
+
"""Base exception for planner-related errors."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str):
|
|
137
|
+
self.message = message
|
|
138
|
+
super().__init__(self.message)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class LLMAPIError(PlannerError):
|
|
142
|
+
"""Raised when an LLM API call fails."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, message: str, provider: str, status_code: int | None = None):
|
|
145
|
+
self.provider = provider
|
|
146
|
+
self.status_code = status_code
|
|
147
|
+
full_message = f"[{provider}] {message}"
|
|
148
|
+
if status_code:
|
|
149
|
+
full_message = f"{full_message} (status: {status_code})"
|
|
150
|
+
super().__init__(full_message)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class LLMTimeoutError(LLMAPIError):
|
|
154
|
+
"""Raised when an LLM API call times out."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, provider: str, timeout_seconds: int):
|
|
157
|
+
self.timeout_seconds = timeout_seconds
|
|
158
|
+
message = f"LLM API call timed out after {timeout_seconds} seconds"
|
|
159
|
+
super().__init__(message, provider)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UDARAgentError(PlannerError):
|
|
163
|
+
"""Base exception for UDAR agent errors."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, message: str, phase: str | None = None):
|
|
166
|
+
self.phase = phase
|
|
167
|
+
full_message = message
|
|
168
|
+
if phase:
|
|
169
|
+
full_message = f"[{phase} phase] {message}"
|
|
170
|
+
super().__init__(full_message)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ToolExecutionError(UDARAgentError):
|
|
174
|
+
"""Raised when a UDAR tool execution fails."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, tool_name: str, message: str, phase: str | None = None):
|
|
177
|
+
self.tool_name = tool_name
|
|
178
|
+
super().__init__(f"Tool '{tool_name}' failed: {message}", phase)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Executor plugin system for Draft."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in executor adapters."""
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Aider AI coding 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("aider")
|
|
22
|
+
class AiderAdapter(ExecutorAdapter):
|
|
23
|
+
"""Aider AI coding assistant adapter."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="aider",
|
|
28
|
+
display_name="Aider",
|
|
29
|
+
version="1.0.0",
|
|
30
|
+
capabilities=[
|
|
31
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
32
|
+
ExecutorCapability.SESSION_RESUME,
|
|
33
|
+
ExecutorCapability.COST_TRACKING,
|
|
34
|
+
],
|
|
35
|
+
config_schema={
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"model": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"default": "gpt-4",
|
|
41
|
+
"description": "LLM model to use",
|
|
42
|
+
},
|
|
43
|
+
"edit_format": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["diff", "whole"],
|
|
46
|
+
"default": "diff",
|
|
47
|
+
"description": "Edit format (diff or whole file)",
|
|
48
|
+
},
|
|
49
|
+
"auto_commits": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"default": True,
|
|
52
|
+
"description": "Auto-commit changes",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
documentation_url="https://aider.chat/docs/",
|
|
57
|
+
author="Aider Project",
|
|
58
|
+
license="Apache-2.0",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def is_available(self) -> bool:
|
|
62
|
+
"""Check if aider is installed."""
|
|
63
|
+
return shutil.which("aider") is not None
|
|
64
|
+
|
|
65
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
66
|
+
"""Execute using Aider."""
|
|
67
|
+
if not await self.is_available():
|
|
68
|
+
raise ExecutorNotFoundError(
|
|
69
|
+
"Aider not found. Install: pip install aider-chat"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Build command
|
|
73
|
+
cmd = [
|
|
74
|
+
"aider",
|
|
75
|
+
"--yes", # Auto-confirm
|
|
76
|
+
"--no-git", # We handle git ourselves
|
|
77
|
+
"--message",
|
|
78
|
+
request.prompt,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Add session resume if provided
|
|
82
|
+
if request.session_id:
|
|
83
|
+
cmd.extend(["--restore-chat-history", request.session_id])
|
|
84
|
+
|
|
85
|
+
# Add model config
|
|
86
|
+
model = request.config.get("model", "gpt-4")
|
|
87
|
+
cmd.extend(["--model", model])
|
|
88
|
+
|
|
89
|
+
# Execute
|
|
90
|
+
try:
|
|
91
|
+
process = await asyncio.create_subprocess_exec(
|
|
92
|
+
*cmd,
|
|
93
|
+
cwd=request.working_directory,
|
|
94
|
+
stdout=asyncio.subprocess.PIPE,
|
|
95
|
+
stderr=asyncio.subprocess.PIPE,
|
|
96
|
+
env={**os.environ, **request.environment},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
stdout, stderr = await asyncio.wait_for(
|
|
100
|
+
process.communicate(), timeout=request.timeout_seconds
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return ExecutionResult(
|
|
104
|
+
exit_code=process.returncode,
|
|
105
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
106
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
107
|
+
files_changed=self._parse_changed_files(stdout.decode()),
|
|
108
|
+
duration_seconds=0.0,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except TimeoutError:
|
|
112
|
+
process.kill()
|
|
113
|
+
raise ExecutorTimeoutError(
|
|
114
|
+
f"Aider execution timed out after {request.timeout_seconds}s"
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise ExecutorInvocationError(f"Aider execution failed: {str(e)}")
|
|
118
|
+
|
|
119
|
+
async def stream_output(self, request: ExecutionRequest) -> AsyncIterator[str]:
|
|
120
|
+
"""Stream output in real-time."""
|
|
121
|
+
if not await self.is_available():
|
|
122
|
+
raise ExecutorNotFoundError("Aider not found")
|
|
123
|
+
|
|
124
|
+
cmd = ["aider", "--yes", "--no-git", "--message", request.prompt]
|
|
125
|
+
|
|
126
|
+
process = await asyncio.create_subprocess_exec(
|
|
127
|
+
*cmd,
|
|
128
|
+
cwd=request.working_directory,
|
|
129
|
+
stdout=asyncio.subprocess.PIPE,
|
|
130
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
131
|
+
env={**os.environ, **request.environment},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
while True:
|
|
135
|
+
line = await process.stdout.readline()
|
|
136
|
+
if not line:
|
|
137
|
+
break
|
|
138
|
+
yield line.decode("utf-8", errors="replace")
|
|
139
|
+
|
|
140
|
+
await process.wait()
|
|
141
|
+
|
|
142
|
+
def _parse_changed_files(self, output: str) -> list[str]:
|
|
143
|
+
"""Parse changed files from Aider output.
|
|
144
|
+
|
|
145
|
+
Aider logs lines like: "Modified path/to/file.py"
|
|
146
|
+
"""
|
|
147
|
+
files = []
|
|
148
|
+
for line in output.split("\n"):
|
|
149
|
+
if line.strip().startswith("Modified "):
|
|
150
|
+
file_path = line.strip()[9:].strip() # Remove "Modified "
|
|
151
|
+
files.append(file_path)
|
|
152
|
+
return files
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Amazon Q Developer 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
|
+
ExecutorTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@ExecutorRegistry.register("amazon-q")
|
|
21
|
+
class AmazonQAdapter(ExecutorAdapter):
|
|
22
|
+
"""Amazon Q Developer adapter."""
|
|
23
|
+
|
|
24
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
25
|
+
return ExecutorMetadata(
|
|
26
|
+
name="amazon-q",
|
|
27
|
+
display_name="Amazon Q Developer",
|
|
28
|
+
version="1.0.0",
|
|
29
|
+
capabilities=[
|
|
30
|
+
ExecutorCapability.STREAMING_OUTPUT,
|
|
31
|
+
],
|
|
32
|
+
config_schema={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"profile": {"type": "string", "description": "AWS profile to use"},
|
|
36
|
+
"model": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": "q-developer",
|
|
39
|
+
"description": "Model variant to use",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
documentation_url="https://aws.amazon.com/q/developer/",
|
|
44
|
+
author="AWS",
|
|
45
|
+
license="Proprietary",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def is_available(self) -> bool:
|
|
49
|
+
"""Check if Q CLI is installed."""
|
|
50
|
+
# Amazon Q can be accessed via `q` or `amazon-q` command
|
|
51
|
+
return shutil.which("q") is not None or shutil.which("amazon-q") is not None
|
|
52
|
+
|
|
53
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
54
|
+
"""Execute using Amazon Q Developer."""
|
|
55
|
+
if not await self.is_available():
|
|
56
|
+
raise ExecutorNotFoundError(
|
|
57
|
+
"Amazon Q not found. Install AWS CLI and Q extension."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Determine which command is available
|
|
61
|
+
cmd_name = "q" if shutil.which("q") else "amazon-q"
|
|
62
|
+
|
|
63
|
+
# Build command
|
|
64
|
+
cmd = [cmd_name, "chat"]
|
|
65
|
+
|
|
66
|
+
if request.yolo_mode:
|
|
67
|
+
cmd.append("--trust-all-tools")
|
|
68
|
+
|
|
69
|
+
# Add AWS profile if specified
|
|
70
|
+
profile = request.config.get("profile")
|
|
71
|
+
if profile:
|
|
72
|
+
cmd.extend(["--profile", profile])
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
process = await asyncio.create_subprocess_exec(
|
|
76
|
+
*cmd,
|
|
77
|
+
cwd=request.working_directory,
|
|
78
|
+
stdin=asyncio.subprocess.PIPE,
|
|
79
|
+
stdout=asyncio.subprocess.PIPE,
|
|
80
|
+
stderr=asyncio.subprocess.PIPE,
|
|
81
|
+
env={**os.environ, **request.environment},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Amazon Q uses stdin for the prompt
|
|
85
|
+
stdout, stderr = await asyncio.wait_for(
|
|
86
|
+
process.communicate(input=request.prompt.encode()),
|
|
87
|
+
timeout=request.timeout_seconds,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return ExecutionResult(
|
|
91
|
+
exit_code=process.returncode,
|
|
92
|
+
stdout=stdout.decode("utf-8", errors="replace"),
|
|
93
|
+
stderr=stderr.decode("utf-8", errors="replace"),
|
|
94
|
+
duration_seconds=0.0,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except TimeoutError:
|
|
98
|
+
process.kill()
|
|
99
|
+
raise ExecutorTimeoutError(
|
|
100
|
+
f"Amazon Q execution timed out after {request.timeout_seconds}s"
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise ExecutorInvocationError(f"Amazon Q execution failed: {str(e)}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Amp (Sourcegraph) 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("amp")
|
|
22
|
+
class AmpAdapter(ExecutorAdapter):
|
|
23
|
+
"""Amp (Sourcegraph) CLI adapter for automated code changes."""
|
|
24
|
+
|
|
25
|
+
def get_metadata(self) -> ExecutorMetadata:
|
|
26
|
+
return ExecutorMetadata(
|
|
27
|
+
name="amp",
|
|
28
|
+
display_name="Amp CLI (Sourcegraph)",
|
|
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 Amp",
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
documentation_url="https://github.com/sourcegraph/amp",
|
|
45
|
+
author="Sourcegraph",
|
|
46
|
+
license="Apache-2.0",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def is_available(self) -> bool:
|
|
50
|
+
"""Check if amp CLI is installed."""
|
|
51
|
+
return shutil.which("amp") is not None
|
|
52
|
+
|
|
53
|
+
async def execute(self, request: ExecutionRequest) -> ExecutionResult:
|
|
54
|
+
"""Execute using Amp CLI."""
|
|
55
|
+
if not await self.is_available():
|
|
56
|
+
raise ExecutorNotFoundError(
|
|
57
|
+
"Amp CLI not found. Install from https://github.com/sourcegraph/amp"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cmd = ["amp", "--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"Amp execution timed out after {request.timeout_seconds}s"
|
|
91
|
+
) from None
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ExecutorInvocationError(f"Amp 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("Amp CLI not found")
|
|
99
|
+
|
|
100
|
+
cmd = ["amp", "--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()
|