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,634 @@
|
|
|
1
|
+
"""Service for executing code changes using CLI tools (Claude, Codex, Gemini, Cursor)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from textwrap import dedent
|
|
9
|
+
|
|
10
|
+
from app.exceptions import ExecutorNotFoundError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExecutorType(StrEnum):
|
|
14
|
+
"""Supported executor CLI types."""
|
|
15
|
+
|
|
16
|
+
CLAUDE = "claude" # Headless executor - can run automatically
|
|
17
|
+
CODEX = "codex" # Headless executor - OpenAI Codex CLI
|
|
18
|
+
GEMINI = "gemini" # Headless executor - Google Gemini CLI
|
|
19
|
+
DROID = "droid" # Headless executor - Droid CLI
|
|
20
|
+
QWEN = "qwen" # Headless executor - Qwen Code CLI
|
|
21
|
+
OPENCODE = "opencode" # Headless executor - OpenCode CLI
|
|
22
|
+
AMP = "amp" # Headless executor - Amp (Sourcegraph) CLI
|
|
23
|
+
CURSOR_AGENT = "cursor-agent" # Headless executor - Cursor Agent CLI
|
|
24
|
+
CURSOR = "cursor" # Interactive executor - requires human completion
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExecutorMode(StrEnum):
|
|
28
|
+
"""Execution mode for the executor."""
|
|
29
|
+
|
|
30
|
+
HEADLESS = "headless" # Fully automated, no human intervention
|
|
31
|
+
INTERACTIVE = "interactive" # Requires human to complete the work
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ExecutorInfo:
|
|
36
|
+
"""Information about an available executor CLI."""
|
|
37
|
+
|
|
38
|
+
executor_type: ExecutorType
|
|
39
|
+
command: str
|
|
40
|
+
path: str
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def mode(self) -> ExecutorMode:
|
|
44
|
+
"""Get the execution mode for this executor.
|
|
45
|
+
|
|
46
|
+
Claude CLI and Cursor Agent CLI support headless operation.
|
|
47
|
+
Cursor CLI is interactive - it opens the editor for human completion.
|
|
48
|
+
"""
|
|
49
|
+
if self.executor_type in (
|
|
50
|
+
ExecutorType.CLAUDE,
|
|
51
|
+
ExecutorType.CODEX,
|
|
52
|
+
ExecutorType.GEMINI,
|
|
53
|
+
ExecutorType.DROID,
|
|
54
|
+
ExecutorType.QWEN,
|
|
55
|
+
ExecutorType.OPENCODE,
|
|
56
|
+
ExecutorType.AMP,
|
|
57
|
+
ExecutorType.CURSOR_AGENT,
|
|
58
|
+
):
|
|
59
|
+
return ExecutorMode.HEADLESS
|
|
60
|
+
return ExecutorMode.INTERACTIVE
|
|
61
|
+
|
|
62
|
+
def is_headless(self) -> bool:
|
|
63
|
+
"""Check if this executor supports headless (non-interactive) operation."""
|
|
64
|
+
return self.mode == ExecutorMode.HEADLESS
|
|
65
|
+
|
|
66
|
+
def is_interactive(self) -> bool:
|
|
67
|
+
"""Check if this executor requires human interaction."""
|
|
68
|
+
return self.mode == ExecutorMode.INTERACTIVE
|
|
69
|
+
|
|
70
|
+
def get_apply_command(
|
|
71
|
+
self,
|
|
72
|
+
prompt_file: Path,
|
|
73
|
+
worktree_path: Path,
|
|
74
|
+
yolo_mode: bool = False,
|
|
75
|
+
**kwargs,
|
|
76
|
+
) -> tuple[list[str], str | None]:
|
|
77
|
+
"""
|
|
78
|
+
Get the command to run for applying changes.
|
|
79
|
+
|
|
80
|
+
Returns a tuple of (command_args, stdin_input). The prompt content is
|
|
81
|
+
passed via stdin instead of as a CLI argument to avoid exceeding
|
|
82
|
+
ARG_MAX (~130KB on most systems) with large prompts.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
prompt_file: Path to the prompt bundle file.
|
|
86
|
+
worktree_path: Path to the worktree directory.
|
|
87
|
+
yolo_mode: If True, use --dangerously-skip-permissions (DANGEROUS).
|
|
88
|
+
Only use when execution is isolated and you accept the risk.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (command args list, stdin content or None).
|
|
92
|
+
"""
|
|
93
|
+
if self.executor_type == ExecutorType.CLAUDE:
|
|
94
|
+
# Claude Code CLI with non-interactive mode:
|
|
95
|
+
# - --print: Non-interactive mode that prints response and exits
|
|
96
|
+
# - --dangerously-skip-permissions: ONLY if yolo_mode is enabled
|
|
97
|
+
# Prompt is piped via stdin to avoid ARG_MAX limits
|
|
98
|
+
prompt_content = prompt_file.read_text()
|
|
99
|
+
cmd = [self.command, "--print"]
|
|
100
|
+
if yolo_mode:
|
|
101
|
+
cmd.append("--dangerously-skip-permissions")
|
|
102
|
+
return cmd, prompt_content
|
|
103
|
+
elif self.executor_type == ExecutorType.CURSOR_AGENT:
|
|
104
|
+
# Cursor Agent CLI with non-interactive mode:
|
|
105
|
+
# - --print: Non-interactive mode that prints response and exits
|
|
106
|
+
# - --output-format=stream-json: Stream JSON output line-by-line for real-time logs
|
|
107
|
+
# - --trust: Trust the workspace directory (required for Cursor Agent to execute)
|
|
108
|
+
# - --force: Allow all commands without prompting (like YOLO mode)
|
|
109
|
+
# - --workspace: Set the working directory
|
|
110
|
+
# Prompt is piped via stdin to avoid ARG_MAX limits
|
|
111
|
+
prompt_content = prompt_file.read_text()
|
|
112
|
+
cmd = [
|
|
113
|
+
self.command,
|
|
114
|
+
"--print",
|
|
115
|
+
"--output-format=stream-json",
|
|
116
|
+
"--trust",
|
|
117
|
+
"--workspace",
|
|
118
|
+
str(worktree_path),
|
|
119
|
+
]
|
|
120
|
+
if yolo_mode:
|
|
121
|
+
cmd.append("--force")
|
|
122
|
+
return cmd, prompt_content
|
|
123
|
+
elif self.executor_type == ExecutorType.CODEX:
|
|
124
|
+
# OpenAI Codex CLI with non-interactive mode:
|
|
125
|
+
# - --print: Non-interactive mode that prints response and exits
|
|
126
|
+
# - --auto-edit: Automatically apply edits to files
|
|
127
|
+
# - --full-auto: ONLY if yolo_mode is enabled (skip all confirmations)
|
|
128
|
+
# Prompt is piped via stdin to avoid ARG_MAX limits
|
|
129
|
+
prompt_content = prompt_file.read_text()
|
|
130
|
+
cmd = [self.command, "--print", "--auto-edit"]
|
|
131
|
+
if yolo_mode:
|
|
132
|
+
cmd.append("--full-auto")
|
|
133
|
+
return cmd, prompt_content
|
|
134
|
+
elif self.executor_type == ExecutorType.GEMINI:
|
|
135
|
+
# Google Gemini CLI with non-interactive mode:
|
|
136
|
+
# - --print: Non-interactive mode that prints response and exits
|
|
137
|
+
# - --yolo: ONLY if yolo_mode is enabled (skip all confirmations)
|
|
138
|
+
# Prompt is piped via stdin to avoid ARG_MAX limits
|
|
139
|
+
prompt_content = prompt_file.read_text()
|
|
140
|
+
cmd = [self.command, "--print"]
|
|
141
|
+
if yolo_mode:
|
|
142
|
+
cmd.append("--yolo")
|
|
143
|
+
return cmd, prompt_content
|
|
144
|
+
elif self.executor_type == ExecutorType.DROID:
|
|
145
|
+
prompt_content = prompt_file.read_text()
|
|
146
|
+
cmd = [self.command, "--print"]
|
|
147
|
+
if yolo_mode:
|
|
148
|
+
cmd.append("--dangerously-skip-permissions")
|
|
149
|
+
return cmd, prompt_content
|
|
150
|
+
elif self.executor_type == ExecutorType.QWEN:
|
|
151
|
+
prompt_content = prompt_file.read_text()
|
|
152
|
+
cmd = [self.command, "--print"]
|
|
153
|
+
if yolo_mode:
|
|
154
|
+
cmd.append("--yolo")
|
|
155
|
+
return cmd, prompt_content
|
|
156
|
+
elif self.executor_type == ExecutorType.OPENCODE:
|
|
157
|
+
prompt_content = prompt_file.read_text()
|
|
158
|
+
cmd = [self.command, "--print"]
|
|
159
|
+
if yolo_mode:
|
|
160
|
+
cmd.append("--yolo")
|
|
161
|
+
return cmd, prompt_content
|
|
162
|
+
elif self.executor_type == ExecutorType.AMP:
|
|
163
|
+
prompt_content = prompt_file.read_text()
|
|
164
|
+
cmd = [self.command, "--print"]
|
|
165
|
+
if yolo_mode:
|
|
166
|
+
cmd.append("--yolo")
|
|
167
|
+
return cmd, prompt_content
|
|
168
|
+
elif self.executor_type == ExecutorType.CURSOR:
|
|
169
|
+
# Cursor CLI is INTERACTIVE ONLY
|
|
170
|
+
# It opens the editor with the worktree. User must complete changes manually.
|
|
171
|
+
# The worker will immediately transition to needs_human.
|
|
172
|
+
return [self.command, str(worktree_path)], None
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(f"Unknown executor type: {self.executor_type}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ExecutorService:
|
|
178
|
+
"""Service for detecting and using code executor CLIs.
|
|
179
|
+
|
|
180
|
+
Executor Types:
|
|
181
|
+
- Claude CLI (headless): Can run fully automated. Preferred for CI/automation.
|
|
182
|
+
- Codex CLI (headless): OpenAI's Codex CLI for automated code changes.
|
|
183
|
+
- Gemini CLI (headless): Google's Gemini CLI for automated code changes.
|
|
184
|
+
- Cursor Agent CLI (headless): Can run fully automated via cursor-agent.
|
|
185
|
+
- Cursor CLI (interactive): Opens editor for human completion. Use as handoff.
|
|
186
|
+
|
|
187
|
+
Design Decisions:
|
|
188
|
+
- Claude CLI is preferred for headless operation
|
|
189
|
+
- Codex and Gemini are alternative headless executors
|
|
190
|
+
- Cursor Agent CLI is another headless executor
|
|
191
|
+
- Cursor CLI is a fallback that prepares workspace + prompt, then hands off to user
|
|
192
|
+
- If only Cursor is available, caller should transition to needs_human
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
# CLI names to check in order of preference
|
|
196
|
+
# Claude, Codex, Gemini, and cursor-agent are preferred because they support headless operation
|
|
197
|
+
CLI_PREFERENCES = [
|
|
198
|
+
(ExecutorType.CLAUDE, "claude"),
|
|
199
|
+
(ExecutorType.CODEX, "codex"),
|
|
200
|
+
(ExecutorType.GEMINI, "gemini"),
|
|
201
|
+
(ExecutorType.DROID, "droid"),
|
|
202
|
+
(ExecutorType.QWEN, "qwen"),
|
|
203
|
+
(ExecutorType.OPENCODE, "opencode"),
|
|
204
|
+
(ExecutorType.AMP, "amp"),
|
|
205
|
+
(ExecutorType.CURSOR_AGENT, "cursor-agent"),
|
|
206
|
+
(ExecutorType.CURSOR, "cursor"),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
# Common paths to check for cursor-agent (not always in PATH)
|
|
210
|
+
CURSOR_AGENT_PATHS = [
|
|
211
|
+
"~/.local/bin/cursor-agent",
|
|
212
|
+
"/usr/local/bin/cursor-agent",
|
|
213
|
+
"/opt/homebrew/bin/cursor-agent",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def _find_cursor_agent(cls, config_path: str | None = None) -> str | None:
|
|
218
|
+
"""Find cursor-agent CLI, checking config path and common locations.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
config_path: Optional custom path from config.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Full path to cursor-agent if found, None otherwise.
|
|
225
|
+
"""
|
|
226
|
+
# Check config path first
|
|
227
|
+
if config_path:
|
|
228
|
+
expanded = os.path.expanduser(config_path)
|
|
229
|
+
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
230
|
+
return expanded
|
|
231
|
+
|
|
232
|
+
# Check common installation paths
|
|
233
|
+
for path in cls.CURSOR_AGENT_PATHS:
|
|
234
|
+
expanded = os.path.expanduser(path)
|
|
235
|
+
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
236
|
+
return expanded
|
|
237
|
+
|
|
238
|
+
# Fall back to PATH
|
|
239
|
+
return shutil.which("cursor-agent")
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def detect_executor(
|
|
243
|
+
cls, preferred: str | None = None, agent_path: str | None = None
|
|
244
|
+
) -> ExecutorInfo:
|
|
245
|
+
"""
|
|
246
|
+
Detect an available executor CLI.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
preferred: Preferred executor type ("cursor" or "claude").
|
|
250
|
+
If specified and available, it will be used.
|
|
251
|
+
agent_path: Custom path for cursor-agent (from config).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
ExecutorInfo with details about the detected CLI.
|
|
255
|
+
IMPORTANT: Check executor_info.is_interactive() - if True, you should
|
|
256
|
+
transition to needs_human instead of expecting automated completion.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ExecutorNotFoundError: If no supported CLI is found.
|
|
260
|
+
"""
|
|
261
|
+
# Build ordered list of executors to check
|
|
262
|
+
cli_order = list(cls.CLI_PREFERENCES)
|
|
263
|
+
|
|
264
|
+
# If a preferred executor is specified, move it to the front
|
|
265
|
+
if preferred:
|
|
266
|
+
preferred_lower = preferred.lower()
|
|
267
|
+
for i, (exec_type, _cmd) in enumerate(cli_order):
|
|
268
|
+
if exec_type.value == preferred_lower:
|
|
269
|
+
cli_order.insert(0, cli_order.pop(i))
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
# Check each CLI in order
|
|
273
|
+
for exec_type, cmd in cli_order:
|
|
274
|
+
if exec_type == ExecutorType.CURSOR_AGENT:
|
|
275
|
+
# Use custom detection for cursor-agent
|
|
276
|
+
path = cls._find_cursor_agent(agent_path)
|
|
277
|
+
else:
|
|
278
|
+
path = shutil.which(cmd)
|
|
279
|
+
|
|
280
|
+
if path:
|
|
281
|
+
return ExecutorInfo(
|
|
282
|
+
executor_type=exec_type,
|
|
283
|
+
command=path, # Use full path for cursor-agent
|
|
284
|
+
path=path,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# No CLI found - raise descriptive error
|
|
288
|
+
raise ExecutorNotFoundError(
|
|
289
|
+
"No supported code executor CLI found. "
|
|
290
|
+
"Please install one of the following:\n"
|
|
291
|
+
" - Claude Code CLI (recommended): "
|
|
292
|
+
"https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview\n"
|
|
293
|
+
" - Codex CLI (OpenAI): https://github.com/openai/codex\n"
|
|
294
|
+
" - Gemini CLI (Google): https://github.com/google/gemini-cli\n"
|
|
295
|
+
" - Droid CLI: https://github.com/anthropics/droid\n"
|
|
296
|
+
" - Qwen Code CLI: https://github.com/QwenLM/qwen-agent\n"
|
|
297
|
+
" - OpenCode CLI: https://github.com/opencode-ai/opencode\n"
|
|
298
|
+
" - Amp CLI (Sourcegraph): https://github.com/sourcegraph/amp\n"
|
|
299
|
+
" - Cursor Agent CLI: Set agent_path in draft.yaml\n"
|
|
300
|
+
" - Cursor CLI (interactive, opens editor): https://docs.cursor.com/cli"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def detect_headless_executor(
|
|
305
|
+
cls, preferred: str | None = None, agent_path: str | None = None
|
|
306
|
+
) -> ExecutorInfo | None:
|
|
307
|
+
"""
|
|
308
|
+
Detect a headless executor CLI only.
|
|
309
|
+
|
|
310
|
+
Unlike detect_executor(), this returns None if only interactive
|
|
311
|
+
executors are available (instead of returning them).
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
preferred: Preferred executor type.
|
|
315
|
+
agent_path: Custom path for cursor-agent (from config).
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
ExecutorInfo if a headless executor is found, None otherwise.
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
executor = cls.detect_executor(preferred=preferred, agent_path=agent_path)
|
|
322
|
+
if executor.is_headless():
|
|
323
|
+
return executor
|
|
324
|
+
return None
|
|
325
|
+
except ExecutorNotFoundError:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def is_available(cls) -> bool:
|
|
330
|
+
"""
|
|
331
|
+
Check if any executor CLI is available.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if at least one supported CLI is available.
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
cls.detect_executor()
|
|
338
|
+
return True
|
|
339
|
+
except ExecutorNotFoundError:
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def is_headless_available(cls) -> bool:
|
|
344
|
+
"""
|
|
345
|
+
Check if a headless executor CLI is available.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
True if at least one headless executor is available.
|
|
349
|
+
"""
|
|
350
|
+
return cls.detect_headless_executor() is not None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class PromptBundleBuilder:
|
|
354
|
+
"""Builder for creating prompt bundles for code execution."""
|
|
355
|
+
|
|
356
|
+
PROMPT_FILENAME = "prompt.txt"
|
|
357
|
+
|
|
358
|
+
def __init__(self, worktree_path: Path, job_id: str, repo_root: Path | None = None):
|
|
359
|
+
"""
|
|
360
|
+
Initialize the prompt bundle builder.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
worktree_path: Path to the worktree directory.
|
|
364
|
+
job_id: UUID of the job.
|
|
365
|
+
repo_root: Path to the main repo root (for persistent evidence storage).
|
|
366
|
+
"""
|
|
367
|
+
from app.data_dir import get_jobs_dir
|
|
368
|
+
|
|
369
|
+
self.worktree_path = worktree_path
|
|
370
|
+
self.job_id = job_id
|
|
371
|
+
self.repo_root = repo_root
|
|
372
|
+
self.job_dir = get_jobs_dir(job_id)
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def prompt_file(self) -> Path:
|
|
376
|
+
"""Get the path to the prompt file."""
|
|
377
|
+
return self.job_dir / self.PROMPT_FILENAME
|
|
378
|
+
|
|
379
|
+
def build_prompt(
|
|
380
|
+
self,
|
|
381
|
+
ticket_title: str,
|
|
382
|
+
ticket_description: str | None,
|
|
383
|
+
additional_context: str | None = None,
|
|
384
|
+
feedback_bundle: dict | None = None,
|
|
385
|
+
related_tickets_context: dict | None = None,
|
|
386
|
+
verify_commands: list[str] | None = None,
|
|
387
|
+
) -> Path:
|
|
388
|
+
"""
|
|
389
|
+
Build a prompt bundle file for the executor CLI.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
ticket_title: Title of the ticket.
|
|
393
|
+
ticket_description: Description of the ticket (may be None).
|
|
394
|
+
additional_context: Optional additional context to include.
|
|
395
|
+
feedback_bundle: Optional feedback from previous revision review.
|
|
396
|
+
related_tickets_context: Optional context about related tickets and dependencies.
|
|
397
|
+
Expected format: {
|
|
398
|
+
"dependencies": [{"title": str, "state": str}], # tickets this depends on
|
|
399
|
+
"completed_tickets": [{"title": str, "description": str}], # already done
|
|
400
|
+
"goal_title": str # optional goal title
|
|
401
|
+
}
|
|
402
|
+
verify_commands: Current verification commands from draft.yaml.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Path to the created prompt file.
|
|
406
|
+
"""
|
|
407
|
+
# Ensure the job directory exists
|
|
408
|
+
self.job_dir.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
|
|
410
|
+
# Build the prompt content
|
|
411
|
+
prompt_content = self._generate_prompt_content(
|
|
412
|
+
ticket_title=ticket_title,
|
|
413
|
+
ticket_description=ticket_description,
|
|
414
|
+
additional_context=additional_context,
|
|
415
|
+
feedback_bundle=feedback_bundle,
|
|
416
|
+
related_tickets_context=related_tickets_context,
|
|
417
|
+
verify_commands=verify_commands,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Write the prompt file
|
|
421
|
+
self.prompt_file.write_text(prompt_content)
|
|
422
|
+
|
|
423
|
+
return self.prompt_file
|
|
424
|
+
|
|
425
|
+
def _generate_prompt_content(
|
|
426
|
+
self,
|
|
427
|
+
ticket_title: str,
|
|
428
|
+
ticket_description: str | None,
|
|
429
|
+
additional_context: str | None = None,
|
|
430
|
+
feedback_bundle: dict | None = None,
|
|
431
|
+
related_tickets_context: dict | None = None,
|
|
432
|
+
verify_commands: list[str] | None = None,
|
|
433
|
+
) -> str:
|
|
434
|
+
"""
|
|
435
|
+
Generate the content for the prompt bundle.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
ticket_title: Title of the ticket.
|
|
439
|
+
ticket_description: Description of the ticket.
|
|
440
|
+
additional_context: Optional additional context.
|
|
441
|
+
feedback_bundle: Optional feedback from previous revision review.
|
|
442
|
+
related_tickets_context: Optional context about related tickets.
|
|
443
|
+
verify_commands: Current verification commands from draft.yaml.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Formatted prompt string.
|
|
447
|
+
"""
|
|
448
|
+
description_text = ticket_description or "No additional description provided."
|
|
449
|
+
|
|
450
|
+
prompt = dedent(f"""\
|
|
451
|
+
# Task: {ticket_title}
|
|
452
|
+
|
|
453
|
+
## Description
|
|
454
|
+
|
|
455
|
+
{description_text}
|
|
456
|
+
""")
|
|
457
|
+
|
|
458
|
+
# Add related tickets context if provided
|
|
459
|
+
if related_tickets_context:
|
|
460
|
+
prompt += self._format_related_tickets_section(related_tickets_context)
|
|
461
|
+
|
|
462
|
+
prompt += dedent("""\
|
|
463
|
+
## Constraints
|
|
464
|
+
|
|
465
|
+
- **CRITICAL**: Analyze the codebase structure FIRST before making changes
|
|
466
|
+
- If the ticket mentions specific file paths (e.g., `app/utils/file.py`), check if that structure exists
|
|
467
|
+
- Adapt paths to match the ACTUAL project structure (don't blindly create new directories)
|
|
468
|
+
- If paths don't match reality, use the existing structure instead
|
|
469
|
+
- Make minimal, focused changes to accomplish the task
|
|
470
|
+
- Do NOT modify files that are unrelated to this task
|
|
471
|
+
- Preserve existing code style and conventions
|
|
472
|
+
- Do NOT introduce unnecessary dependencies
|
|
473
|
+
- Keep changes atomic and reviewable
|
|
474
|
+
""")
|
|
475
|
+
|
|
476
|
+
# Add revision feedback if present
|
|
477
|
+
if feedback_bundle:
|
|
478
|
+
prompt += self._format_feedback_section(feedback_bundle)
|
|
479
|
+
|
|
480
|
+
prompt += dedent("""\
|
|
481
|
+
## Completion Criteria
|
|
482
|
+
|
|
483
|
+
- Code compiles without errors
|
|
484
|
+
- Tests pass (if applicable)
|
|
485
|
+
- Changes are minimal and focused on the task
|
|
486
|
+
- No unrelated modifications
|
|
487
|
+
""")
|
|
488
|
+
|
|
489
|
+
if feedback_bundle:
|
|
490
|
+
prompt += " - All review feedback has been addressed\n"
|
|
491
|
+
|
|
492
|
+
prompt += dedent("""\
|
|
493
|
+
## Instructions
|
|
494
|
+
|
|
495
|
+
1. **First, explore the codebase** to understand:
|
|
496
|
+
- The current directory structure
|
|
497
|
+
- Naming conventions and patterns
|
|
498
|
+
- Where similar functionality already exists
|
|
499
|
+
- Dependencies and existing modules
|
|
500
|
+
|
|
501
|
+
2. **Validate the approach**:
|
|
502
|
+
- If the ticket mentions specific paths, verify they match the actual structure
|
|
503
|
+
- If paths don't exist, decide: create them OR adapt to existing structure
|
|
504
|
+
- Choose the approach that's most consistent with the codebase
|
|
505
|
+
|
|
506
|
+
3. **Implement the changes** described in the task
|
|
507
|
+
|
|
508
|
+
4. **Provide a summary** explaining:
|
|
509
|
+
- What files were modified or created
|
|
510
|
+
- What changes were made
|
|
511
|
+
- Why each change was necessary
|
|
512
|
+
- Any path adaptations you made from the ticket description
|
|
513
|
+
""")
|
|
514
|
+
|
|
515
|
+
# Add verification scoping instructions
|
|
516
|
+
if verify_commands:
|
|
517
|
+
commands_str = "\n".join(f" - `{cmd}`" for cmd in verify_commands)
|
|
518
|
+
prompt += dedent(f"""\
|
|
519
|
+
## Verification Setup
|
|
520
|
+
|
|
521
|
+
After implementing your changes, the following verification commands will run:
|
|
522
|
+
{commands_str}
|
|
523
|
+
|
|
524
|
+
**IMPORTANT**: If the verification commands above run a broad test suite (e.g., the
|
|
525
|
+
entire test file), you MUST update `draft.yaml` in this worktree to scope the
|
|
526
|
+
`verify_config.commands` to ONLY the tests relevant to your changes. This prevents
|
|
527
|
+
unrelated test failures from blocking your ticket.
|
|
528
|
+
|
|
529
|
+
For example, if you fixed `fibonacci` and `is_prime`, update the verify config to:
|
|
530
|
+
```yaml
|
|
531
|
+
verify_config:
|
|
532
|
+
commands:
|
|
533
|
+
- "python -m pytest -q test_calculator.py::TestFibonacci test_calculator.py::TestIsPrime"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Scope the verify commands to the test classes/functions that cover your changes.
|
|
537
|
+
""")
|
|
538
|
+
|
|
539
|
+
if additional_context:
|
|
540
|
+
prompt += f"\n## Additional Context\n\n{additional_context}\n"
|
|
541
|
+
|
|
542
|
+
return prompt
|
|
543
|
+
|
|
544
|
+
def _format_related_tickets_section(self, related_tickets_context: dict) -> str:
|
|
545
|
+
"""
|
|
546
|
+
Format related tickets context as a prompt section.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
related_tickets_context: Dictionary with dependencies and completed tickets.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Formatted related tickets section for the prompt.
|
|
553
|
+
"""
|
|
554
|
+
section = "\n## Related Tickets Context\n\n"
|
|
555
|
+
|
|
556
|
+
goal_title = related_tickets_context.get("goal_title")
|
|
557
|
+
if goal_title:
|
|
558
|
+
section += f"**Goal**: {goal_title}\n\n"
|
|
559
|
+
|
|
560
|
+
# Add completed tickets for context
|
|
561
|
+
completed_tickets = related_tickets_context.get("completed_tickets", [])
|
|
562
|
+
if completed_tickets:
|
|
563
|
+
section += "### Previously Completed Tickets\n\n"
|
|
564
|
+
section += "These tickets in the same goal have already been completed:\n\n"
|
|
565
|
+
for ticket in completed_tickets:
|
|
566
|
+
section += f"- **{ticket['title']}**"
|
|
567
|
+
if ticket.get("description"):
|
|
568
|
+
# Truncate long descriptions
|
|
569
|
+
desc = ticket["description"]
|
|
570
|
+
if len(desc) > 150:
|
|
571
|
+
desc = desc[:150] + "..."
|
|
572
|
+
section += f": {desc}"
|
|
573
|
+
section += "\n"
|
|
574
|
+
section += "\n**Important**: Build upon this existing work. Don't recreate what's already done.\n\n"
|
|
575
|
+
|
|
576
|
+
# Add dependency information
|
|
577
|
+
dependencies = related_tickets_context.get("dependencies", [])
|
|
578
|
+
if dependencies:
|
|
579
|
+
section += "### Dependencies\n\n"
|
|
580
|
+
section += (
|
|
581
|
+
"This ticket depends on the following tickets being completed:\n\n"
|
|
582
|
+
)
|
|
583
|
+
for dep in dependencies:
|
|
584
|
+
section += f"- **{dep['title']}** (Status: {dep['state']})\n"
|
|
585
|
+
section += "\n**Note**: You can assume dependencies are complete and build upon their work.\n\n"
|
|
586
|
+
|
|
587
|
+
return section
|
|
588
|
+
|
|
589
|
+
def _format_feedback_section(self, feedback_bundle: dict) -> str:
|
|
590
|
+
"""
|
|
591
|
+
Format the feedback bundle as a prompt section.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
feedback_bundle: The feedback bundle dict.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Formatted feedback section for the prompt.
|
|
598
|
+
"""
|
|
599
|
+
section = "\n## Previous Revision Feedback\n\n"
|
|
600
|
+
section += f"**Revision #{feedback_bundle.get('revision_number', '?')} was reviewed and changes were requested.**\n\n"
|
|
601
|
+
|
|
602
|
+
# Add overall summary
|
|
603
|
+
summary = feedback_bundle.get("summary", "")
|
|
604
|
+
if summary:
|
|
605
|
+
section += f"### Reviewer Summary\n\n{summary}\n\n"
|
|
606
|
+
|
|
607
|
+
# Add inline comments
|
|
608
|
+
comments = feedback_bundle.get("comments", [])
|
|
609
|
+
if comments:
|
|
610
|
+
section += "### Inline Comments to Address\n\n"
|
|
611
|
+
for comment in comments:
|
|
612
|
+
file_path = comment.get("file_path", "unknown")
|
|
613
|
+
line_number = comment.get("line_number", "?")
|
|
614
|
+
body = comment.get("body", "")
|
|
615
|
+
line_content = comment.get("line_content", "")
|
|
616
|
+
section += f"- **{file_path}:{line_number}**: {body}\n"
|
|
617
|
+
if line_content:
|
|
618
|
+
section += f" - Line content: `{line_content}`\n"
|
|
619
|
+
section += "\n"
|
|
620
|
+
|
|
621
|
+
section += "**Important**: Address ALL feedback above while preserving correct changes from the previous revision.\n\n"
|
|
622
|
+
|
|
623
|
+
return section
|
|
624
|
+
|
|
625
|
+
def get_evidence_dir(self) -> Path:
|
|
626
|
+
"""
|
|
627
|
+
Get the evidence directory for this job (central location).
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
Path to the evidence directory.
|
|
631
|
+
"""
|
|
632
|
+
from app.data_dir import get_evidence_dir as _get_evidence_dir
|
|
633
|
+
|
|
634
|
+
return _get_evidence_dir(self.job_id)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Git host abstraction layer for GitHub and GitLab."""
|
|
2
|
+
|
|
3
|
+
from app.services.git_host.factory import detect_git_host, get_git_host_provider
|
|
4
|
+
from app.services.git_host.protocol import GitHostProvider, PullRequest
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"GitHostProvider",
|
|
8
|
+
"PullRequest",
|
|
9
|
+
"get_git_host_provider",
|
|
10
|
+
"detect_git_host",
|
|
11
|
+
]
|