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,500 @@
|
|
|
1
|
+
"""Pydantic schemas for AI Planner feature."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# Validation Constants
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
MAX_VERIFICATION_COMMANDS = 5
|
|
14
|
+
MAX_COMMAND_LENGTH = 500
|
|
15
|
+
FORBIDDEN_PATTERNS = [
|
|
16
|
+
rb"\x00", # Null bytes
|
|
17
|
+
rb"[\x00-\x08\x0b\x0c\x0e-\x1f]", # Control characters (except tab, newline, carriage return)
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_verification_command(cmd: str) -> str:
|
|
22
|
+
"""Validate a single verification command."""
|
|
23
|
+
if len(cmd) > MAX_COMMAND_LENGTH:
|
|
24
|
+
raise ValueError(f"Command exceeds {MAX_COMMAND_LENGTH} characters")
|
|
25
|
+
|
|
26
|
+
cmd_bytes = cmd.encode("utf-8", errors="replace")
|
|
27
|
+
for pattern in FORBIDDEN_PATTERNS:
|
|
28
|
+
if re.search(pattern, cmd_bytes):
|
|
29
|
+
raise ValueError("Command contains forbidden control characters")
|
|
30
|
+
|
|
31
|
+
return cmd.strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Priority Bucket System
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PriorityBucket(StrEnum):
|
|
40
|
+
"""Priority buckets for deterministic prioritization.
|
|
41
|
+
|
|
42
|
+
Using buckets instead of raw 0-100 values prevents LLM "priority inflation"
|
|
43
|
+
where everything ends up in the 80-90 range.
|
|
44
|
+
|
|
45
|
+
Numeric values are derived from buckets:
|
|
46
|
+
- P0 = 90 (Critical: security issues, blocking bugs)
|
|
47
|
+
- P1 = 70 (High: important features, performance)
|
|
48
|
+
- P2 = 50 (Medium: improvements, nice-to-haves)
|
|
49
|
+
- P3 = 30 (Low: cleanup, documentation)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
P0 = "P0" # Critical -> 90
|
|
53
|
+
P1 = "P1" # High -> 70
|
|
54
|
+
P2 = "P2" # Medium -> 50
|
|
55
|
+
P3 = "P3" # Low -> 30
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Bucket to numeric priority mapping
|
|
59
|
+
PRIORITY_BUCKET_VALUES = {
|
|
60
|
+
PriorityBucket.P0: 90,
|
|
61
|
+
PriorityBucket.P1: 70,
|
|
62
|
+
PriorityBucket.P2: 50,
|
|
63
|
+
PriorityBucket.P3: 30,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bucket_to_priority(bucket: PriorityBucket | str) -> int:
|
|
68
|
+
"""Convert a priority bucket to a numeric priority value."""
|
|
69
|
+
if isinstance(bucket, str):
|
|
70
|
+
bucket = PriorityBucket(bucket)
|
|
71
|
+
return PRIORITY_BUCKET_VALUES[bucket]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def priority_to_bucket(priority: int) -> PriorityBucket:
|
|
75
|
+
"""Convert a numeric priority to the nearest bucket."""
|
|
76
|
+
if priority >= 80:
|
|
77
|
+
return PriorityBucket.P0
|
|
78
|
+
elif priority >= 60:
|
|
79
|
+
return PriorityBucket.P1
|
|
80
|
+
elif priority >= 40:
|
|
81
|
+
return PriorityBucket.P2
|
|
82
|
+
else:
|
|
83
|
+
return PriorityBucket.P3
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Ticket Generation Schemas
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ProposedTicketSchema(BaseModel):
|
|
92
|
+
"""Schema for a single proposed ticket from the LLM planner."""
|
|
93
|
+
|
|
94
|
+
title: str = Field(..., min_length=1, max_length=255)
|
|
95
|
+
description: str = Field(..., min_length=1)
|
|
96
|
+
verification: list[str] = Field(
|
|
97
|
+
default_factory=list,
|
|
98
|
+
description="Commands to verify the ticket implementation",
|
|
99
|
+
max_length=MAX_VERIFICATION_COMMANDS,
|
|
100
|
+
)
|
|
101
|
+
notes: str | None = Field(None, description="Optional context or notes")
|
|
102
|
+
blocked_by: str | None = Field(
|
|
103
|
+
None,
|
|
104
|
+
description="Title of another ticket in this batch that blocks this one. "
|
|
105
|
+
"The ticket cannot be executed until the blocker is DONE.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@field_validator("verification", mode="before")
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_verification_commands(cls, v: list[str] | None) -> list[str]:
|
|
111
|
+
"""Validate and sanitize verification commands."""
|
|
112
|
+
if not v:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
if not isinstance(v, list):
|
|
116
|
+
raise ValueError(f"verification must be a list, got {type(v).__name__}")
|
|
117
|
+
|
|
118
|
+
if len(v) > MAX_VERIFICATION_COMMANDS:
|
|
119
|
+
v = v[:MAX_VERIFICATION_COMMANDS]
|
|
120
|
+
|
|
121
|
+
validated = []
|
|
122
|
+
for cmd in v:
|
|
123
|
+
if not isinstance(cmd, str):
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
validated.append(validate_verification_command(cmd))
|
|
127
|
+
except ValueError:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
return validated
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class GeneratedTicket(BaseModel):
|
|
134
|
+
"""Schema for a generated ticket with priority bucket."""
|
|
135
|
+
|
|
136
|
+
title: str = Field(..., min_length=1, max_length=255)
|
|
137
|
+
description: str = Field(..., min_length=1)
|
|
138
|
+
priority_bucket: PriorityBucket = Field(..., description="Priority bucket (P0-P3)")
|
|
139
|
+
priority: int = Field(..., ge=0, le=100, description="Derived numeric priority")
|
|
140
|
+
priority_rationale: str = Field(
|
|
141
|
+
..., description="Explanation for the assigned priority"
|
|
142
|
+
)
|
|
143
|
+
verification: list[str] = Field(default_factory=list)
|
|
144
|
+
notes: str | None = None
|
|
145
|
+
blocked_by: str | None = Field(
|
|
146
|
+
None,
|
|
147
|
+
description="Title of another ticket that blocks this one",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@field_validator("verification", mode="before")
|
|
151
|
+
@classmethod
|
|
152
|
+
def validate_verification_commands(cls, v: list[str] | None) -> list[str]:
|
|
153
|
+
"""Validate and sanitize verification commands."""
|
|
154
|
+
if not v:
|
|
155
|
+
return []
|
|
156
|
+
if not isinstance(v, list):
|
|
157
|
+
return []
|
|
158
|
+
validated = []
|
|
159
|
+
for cmd in v[:MAX_VERIFICATION_COMMANDS]:
|
|
160
|
+
if isinstance(cmd, str):
|
|
161
|
+
try:
|
|
162
|
+
validated.append(validate_verification_command(cmd))
|
|
163
|
+
except ValueError:
|
|
164
|
+
continue
|
|
165
|
+
return validated
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class GenerateTicketsRequest(BaseModel):
|
|
169
|
+
"""Request schema for generating tickets from a goal.
|
|
170
|
+
|
|
171
|
+
SECURITY NOTE: The repository path is ALWAYS inferred from server config
|
|
172
|
+
(draft.yaml repo_root). This prevents directory traversal attacks.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
model_config = ConfigDict(extra="ignore")
|
|
176
|
+
|
|
177
|
+
include_readme: bool = Field(
|
|
178
|
+
default=False,
|
|
179
|
+
description="Whether to include README excerpt in context",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class CreatedTicketSchema(BaseModel):
|
|
184
|
+
"""Schema for a created ticket with its ID."""
|
|
185
|
+
|
|
186
|
+
id: str = Field(..., description="ID of the created ticket")
|
|
187
|
+
title: str
|
|
188
|
+
description: str
|
|
189
|
+
priority_bucket: PriorityBucket | None = Field(
|
|
190
|
+
None, description="Priority bucket if generated with buckets"
|
|
191
|
+
)
|
|
192
|
+
priority: int | None = Field(None, description="Numeric priority (0-100)")
|
|
193
|
+
priority_rationale: str | None = Field(
|
|
194
|
+
None, description="Explanation for the assigned priority"
|
|
195
|
+
)
|
|
196
|
+
verification: list[str] = Field(default_factory=list)
|
|
197
|
+
notes: str | None = None
|
|
198
|
+
blocked_by_ticket_id: str | None = Field(
|
|
199
|
+
None, description="ID of the ticket that blocks this one"
|
|
200
|
+
)
|
|
201
|
+
blocked_by_title: str | None = Field(
|
|
202
|
+
None, description="Title of the ticket that blocks this one (for display)"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class GenerateTicketsResponse(BaseModel):
|
|
207
|
+
"""Response schema containing generated proposed tickets."""
|
|
208
|
+
|
|
209
|
+
tickets: list[CreatedTicketSchema] = Field(
|
|
210
|
+
default_factory=list,
|
|
211
|
+
description="List of proposed tickets created by the planner",
|
|
212
|
+
)
|
|
213
|
+
goal_id: str = Field(..., description="ID of the goal these tickets belong to")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class LLMTicketsResponse(BaseModel):
|
|
217
|
+
"""Schema for parsing the raw LLM JSON response."""
|
|
218
|
+
|
|
219
|
+
tickets: list[ProposedTicketSchema]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# =============================================================================
|
|
223
|
+
# Codebase Analysis Schemas
|
|
224
|
+
# =============================================================================
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ExcludedMatch(BaseModel):
|
|
228
|
+
"""A pattern that caused files to be excluded."""
|
|
229
|
+
|
|
230
|
+
pattern: str = Field(..., description="The exclusion pattern that matched")
|
|
231
|
+
count: int = Field(..., description="Number of files excluded by this pattern")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class FiletypeCount(BaseModel):
|
|
235
|
+
"""Count of files by extension/type."""
|
|
236
|
+
|
|
237
|
+
extension: str = Field(..., description="File extension (e.g., '.py', '.ts')")
|
|
238
|
+
count: int = Field(..., description="Number of files with this extension")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ContextStats(BaseModel):
|
|
242
|
+
"""Statistics from context gathering for observability and debugging."""
|
|
243
|
+
|
|
244
|
+
files_scanned: int = Field(0, description="Number of files scanned")
|
|
245
|
+
todos_collected: int = Field(0, description="Number of TODO/FIXME comments found")
|
|
246
|
+
context_truncated: bool = Field(
|
|
247
|
+
False, description="Whether context was truncated due to caps"
|
|
248
|
+
)
|
|
249
|
+
skipped_excluded: int = Field(0, description="Files skipped due to exclusion rules")
|
|
250
|
+
skipped_symlinks: int = Field(0, description="Symlinks skipped for security")
|
|
251
|
+
bytes_read: int = Field(0, description="Total bytes read from files")
|
|
252
|
+
# New observability fields
|
|
253
|
+
excluded_matches: list[ExcludedMatch] = Field(
|
|
254
|
+
default_factory=list,
|
|
255
|
+
description="Top exclusion patterns hit (capped to 10)",
|
|
256
|
+
)
|
|
257
|
+
filetype_histogram: list[FiletypeCount] = Field(
|
|
258
|
+
default_factory=list,
|
|
259
|
+
description="Top file extensions scanned (capped to 10)",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class SimilarTicketWarning(BaseModel):
|
|
264
|
+
"""Warning about a ticket that was similar to an existing one."""
|
|
265
|
+
|
|
266
|
+
proposed_title: str = Field(..., description="Title of the proposed ticket")
|
|
267
|
+
similar_to_id: str = Field(..., description="ID of the similar existing ticket")
|
|
268
|
+
similar_to_title: str = Field(
|
|
269
|
+
..., description="Title of the similar existing ticket"
|
|
270
|
+
)
|
|
271
|
+
similarity_score: float = Field(..., description="Similarity score (0-1)")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class AnalyzeCodebaseRequest(BaseModel):
|
|
275
|
+
"""Request schema for analyzing a codebase to generate tickets."""
|
|
276
|
+
|
|
277
|
+
goal_id: str | None = Field(
|
|
278
|
+
None,
|
|
279
|
+
description="If provided, attach generated tickets to this goal",
|
|
280
|
+
)
|
|
281
|
+
focus_areas: list[str] | None = Field(
|
|
282
|
+
None,
|
|
283
|
+
description="Optional focus hints: ['security', 'performance', 'tests', 'docs']",
|
|
284
|
+
)
|
|
285
|
+
include_readme: bool = Field(
|
|
286
|
+
default=False,
|
|
287
|
+
description="Whether to include README excerpt in analysis",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class AnalyzeCodebaseResponse(BaseModel):
|
|
292
|
+
"""Response schema for codebase analysis."""
|
|
293
|
+
|
|
294
|
+
tickets: list[CreatedTicketSchema] = Field(
|
|
295
|
+
default_factory=list,
|
|
296
|
+
description="Generated tickets from codebase analysis",
|
|
297
|
+
)
|
|
298
|
+
goal_id: str | None = Field(
|
|
299
|
+
None,
|
|
300
|
+
description="Goal ID if tickets were attached to a goal",
|
|
301
|
+
)
|
|
302
|
+
analysis_summary: str = Field(
|
|
303
|
+
...,
|
|
304
|
+
description="High-level summary of codebase findings",
|
|
305
|
+
)
|
|
306
|
+
cache_hit: bool = Field(
|
|
307
|
+
default=False,
|
|
308
|
+
description="Whether this result was served from cache",
|
|
309
|
+
)
|
|
310
|
+
context_stats: ContextStats | None = Field(
|
|
311
|
+
None,
|
|
312
|
+
description="Statistics from context gathering (quality indicators)",
|
|
313
|
+
)
|
|
314
|
+
similar_warnings: list[SimilarTicketWarning] = Field(
|
|
315
|
+
default_factory=list,
|
|
316
|
+
description="Tickets skipped due to similarity (not exact match)",
|
|
317
|
+
)
|
|
318
|
+
repo_head_sha: str | None = Field(
|
|
319
|
+
None,
|
|
320
|
+
description="Git HEAD SHA of main repo at time of analysis (full 40-char SHA)",
|
|
321
|
+
)
|
|
322
|
+
workspace_head_sha: str | None = Field(
|
|
323
|
+
None,
|
|
324
|
+
description="Git HEAD SHA of workspace if different from repo root (for worktrees)",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# =============================================================================
|
|
329
|
+
# Reflection Schemas
|
|
330
|
+
# =============================================================================
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class SuggestedPriorityChange(BaseModel):
|
|
334
|
+
"""A suggested change to a ticket's priority."""
|
|
335
|
+
|
|
336
|
+
ticket_id: str
|
|
337
|
+
ticket_title: str
|
|
338
|
+
current_bucket: PriorityBucket
|
|
339
|
+
current_priority: int
|
|
340
|
+
suggested_bucket: PriorityBucket
|
|
341
|
+
suggested_priority: int
|
|
342
|
+
reason: str
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class ReflectionResult(BaseModel):
|
|
346
|
+
"""Result of reflecting on proposed tickets."""
|
|
347
|
+
|
|
348
|
+
overall_quality: Literal["good", "needs_work", "insufficient"] = Field(
|
|
349
|
+
..., description="Overall assessment of ticket quality"
|
|
350
|
+
)
|
|
351
|
+
quality_notes: str = Field(..., description="Detailed notes on ticket quality")
|
|
352
|
+
coverage_gaps: list[str] = Field(
|
|
353
|
+
default_factory=list,
|
|
354
|
+
description="Areas not covered by current tickets",
|
|
355
|
+
)
|
|
356
|
+
suggested_changes: list[SuggestedPriorityChange] = Field(
|
|
357
|
+
default_factory=list,
|
|
358
|
+
description="Suggested priority adjustments",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# =============================================================================
|
|
363
|
+
# Bulk Priority Update Schemas
|
|
364
|
+
# =============================================================================
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class PriorityUpdate(BaseModel):
|
|
368
|
+
"""A single priority update request."""
|
|
369
|
+
|
|
370
|
+
ticket_id: str
|
|
371
|
+
priority_bucket: PriorityBucket
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class BulkPriorityUpdateRequest(BaseModel):
|
|
375
|
+
"""Request to update priorities for multiple tickets.
|
|
376
|
+
|
|
377
|
+
AUTHORIZATION: board_id is REQUIRED. All tickets must belong to this board.
|
|
378
|
+
|
|
379
|
+
SAFETY: P0 assignments require explicit allow_p0=true flag and are
|
|
380
|
+
limited to MAX_P0_PER_REQUEST (default 3) per request.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
board_id: str = Field(
|
|
384
|
+
...,
|
|
385
|
+
description="Board ID - all tickets must belong to this board (authorization boundary)",
|
|
386
|
+
)
|
|
387
|
+
goal_id: str = Field(
|
|
388
|
+
...,
|
|
389
|
+
description="Goal ID - all tickets must belong to this goal",
|
|
390
|
+
)
|
|
391
|
+
updates: list[PriorityUpdate] = Field(
|
|
392
|
+
..., min_length=1, description="List of priority updates to apply"
|
|
393
|
+
)
|
|
394
|
+
allow_p0: bool = Field(
|
|
395
|
+
default=False,
|
|
396
|
+
description="Must be true to assign P0 priority. Safety guard against accidental critical escalation.",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# Server-side P0 safety limits
|
|
401
|
+
MAX_P0_PER_REQUEST = 3
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class BulkPriorityUpdateResult(BaseModel):
|
|
405
|
+
"""Result for a single ticket in bulk priority update."""
|
|
406
|
+
|
|
407
|
+
ticket_id: str
|
|
408
|
+
success: bool
|
|
409
|
+
new_priority: int | None = None
|
|
410
|
+
new_bucket: PriorityBucket | None = None
|
|
411
|
+
error: str | None = None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class BulkPriorityUpdateResponse(BaseModel):
|
|
415
|
+
"""Response for bulk priority update operation."""
|
|
416
|
+
|
|
417
|
+
updated: list[BulkPriorityUpdateResult] = Field(default_factory=list)
|
|
418
|
+
updated_count: int
|
|
419
|
+
failed_count: int
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# =============================================================================
|
|
423
|
+
# Planner Tick Schemas
|
|
424
|
+
# =============================================================================
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class PlannerActionType(str):
|
|
428
|
+
"""Types of actions the planner can take."""
|
|
429
|
+
|
|
430
|
+
ENQUEUED_EXECUTE = "enqueued_execute"
|
|
431
|
+
PROPOSED_FOLLOWUP = "proposed_followup"
|
|
432
|
+
GENERATED_REFLECTION = "generated_reflection"
|
|
433
|
+
SKIPPED = "skipped"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class PlannerAction(BaseModel):
|
|
437
|
+
"""A single action taken by the planner during a tick."""
|
|
438
|
+
|
|
439
|
+
action_type: str = Field(..., description="Type of action taken")
|
|
440
|
+
ticket_id: str = Field(..., description="ID of the ticket affected")
|
|
441
|
+
ticket_title: str | None = Field(
|
|
442
|
+
None, description="Title of the ticket for display"
|
|
443
|
+
)
|
|
444
|
+
details: dict | None = Field(
|
|
445
|
+
None, description="Additional details about the action"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class PlannerTickResponse(BaseModel):
|
|
450
|
+
"""Response from a planner tick operation."""
|
|
451
|
+
|
|
452
|
+
actions: list[PlannerAction] = Field(
|
|
453
|
+
default_factory=list,
|
|
454
|
+
description="List of actions taken during this tick",
|
|
455
|
+
)
|
|
456
|
+
summary: str = Field(
|
|
457
|
+
...,
|
|
458
|
+
description="Human-readable summary of what happened",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class PlannerTickRequest(BaseModel):
|
|
463
|
+
"""Request for a planner tick operation (currently empty, for future expansion)."""
|
|
464
|
+
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class PlannerStartRequest(BaseModel):
|
|
469
|
+
"""Request for starting the autopilot loop."""
|
|
470
|
+
|
|
471
|
+
poll_interval_seconds: int = Field(
|
|
472
|
+
default=5,
|
|
473
|
+
ge=1,
|
|
474
|
+
le=60,
|
|
475
|
+
description="Seconds to wait between checking queue status",
|
|
476
|
+
)
|
|
477
|
+
max_duration_seconds: int = Field(
|
|
478
|
+
default=3600,
|
|
479
|
+
ge=60,
|
|
480
|
+
le=86400,
|
|
481
|
+
description="Maximum time to run the autopilot loop (1 hour default, max 24 hours)",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class PlannerStartResponse(BaseModel):
|
|
486
|
+
"""Response from starting the autopilot."""
|
|
487
|
+
|
|
488
|
+
status: str = Field(..., description="'running', 'completed', or 'error'")
|
|
489
|
+
message: str = Field(..., description="Human-readable status message")
|
|
490
|
+
tickets_queued: int = Field(
|
|
491
|
+
default=0, description="Number of tickets initially queued"
|
|
492
|
+
)
|
|
493
|
+
tickets_completed: int = Field(default=0, description="Number of tickets completed")
|
|
494
|
+
tickets_failed: int = Field(
|
|
495
|
+
default=0, description="Number of tickets that failed/blocked"
|
|
496
|
+
)
|
|
497
|
+
total_actions: list[PlannerAction] = Field(
|
|
498
|
+
default_factory=list,
|
|
499
|
+
description="All actions taken during the autopilot run",
|
|
500
|
+
)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Pydantic schemas for Repo and BoardRepo models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# Repo schemas
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RepoBase(BaseModel):
|
|
13
|
+
"""Base schema for Repo."""
|
|
14
|
+
|
|
15
|
+
path: str = Field(..., description="Filesystem path to git repository")
|
|
16
|
+
name: str = Field(..., description="Repository name (derived from path)")
|
|
17
|
+
display_name: str = Field(..., description="User-friendly display name")
|
|
18
|
+
setup_script: str | None = Field(None, description="Optional setup script")
|
|
19
|
+
cleanup_script: str | None = Field(None, description="Optional cleanup script")
|
|
20
|
+
dev_server_script: str | None = Field(
|
|
21
|
+
None, description="Optional dev server script"
|
|
22
|
+
)
|
|
23
|
+
default_branch: str | None = Field(None, description="Default git branch")
|
|
24
|
+
remote_url: str | None = Field(None, description="Git remote URL")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RepoCreate(BaseModel):
|
|
28
|
+
"""Schema for creating a new repo."""
|
|
29
|
+
|
|
30
|
+
path: str = Field(..., description="Filesystem path to git repository")
|
|
31
|
+
display_name: str | None = Field(
|
|
32
|
+
None, description="Optional display name (defaults to repo name)"
|
|
33
|
+
)
|
|
34
|
+
setup_script: str | None = Field(None, description="Optional setup script")
|
|
35
|
+
cleanup_script: str | None = Field(None, description="Optional cleanup script")
|
|
36
|
+
dev_server_script: str | None = Field(
|
|
37
|
+
None, description="Optional dev server script"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RepoUpdate(BaseModel):
|
|
42
|
+
"""Schema for updating a repo."""
|
|
43
|
+
|
|
44
|
+
display_name: str | None = Field(None, description="User-friendly display name")
|
|
45
|
+
setup_script: str | None = Field(None, description="Optional setup script")
|
|
46
|
+
cleanup_script: str | None = Field(None, description="Optional cleanup script")
|
|
47
|
+
dev_server_script: str | None = Field(
|
|
48
|
+
None, description="Optional dev server script"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RepoResponse(RepoBase):
|
|
53
|
+
"""Schema for repo API responses."""
|
|
54
|
+
|
|
55
|
+
id: str = Field(..., description="Repo UUID")
|
|
56
|
+
created_at: datetime = Field(..., description="When repo was registered")
|
|
57
|
+
updated_at: datetime = Field(..., description="When repo was last updated")
|
|
58
|
+
|
|
59
|
+
class Config:
|
|
60
|
+
from_attributes = True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RepoListResponse(BaseModel):
|
|
64
|
+
"""Schema for repo list API response."""
|
|
65
|
+
|
|
66
|
+
repos: list[RepoResponse] = Field(..., description="List of repos")
|
|
67
|
+
total: int = Field(..., description="Total number of repos")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ============================================================================
|
|
71
|
+
# Discovery schemas
|
|
72
|
+
# ============================================================================
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DiscoveredRepoResponse(BaseModel):
|
|
76
|
+
"""Schema for discovered repo (not yet registered)."""
|
|
77
|
+
|
|
78
|
+
path: str = Field(..., description="Filesystem path to git repository")
|
|
79
|
+
name: str = Field(..., description="Repository name")
|
|
80
|
+
display_name: str = Field(..., description="Display name")
|
|
81
|
+
default_branch: str | None = Field(None, description="Default git branch")
|
|
82
|
+
remote_url: str | None = Field(None, description="Git remote URL")
|
|
83
|
+
is_valid: bool = Field(..., description="Whether repo is valid")
|
|
84
|
+
error_message: str | None = Field(None, description="Error message if invalid")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DiscoverReposRequest(BaseModel):
|
|
88
|
+
"""Schema for repo discovery request."""
|
|
89
|
+
|
|
90
|
+
search_paths: list[str] = Field(
|
|
91
|
+
..., description="Paths to scan for git repositories"
|
|
92
|
+
)
|
|
93
|
+
max_depth: int = Field(
|
|
94
|
+
3, description="Maximum directory depth to scan", ge=1, le=10
|
|
95
|
+
)
|
|
96
|
+
exclude_patterns: list[str] | None = Field(
|
|
97
|
+
None, description="Additional patterns to exclude"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DiscoverReposResponse(BaseModel):
|
|
102
|
+
"""Schema for repo discovery response."""
|
|
103
|
+
|
|
104
|
+
discovered: list[DiscoveredRepoResponse] = Field(
|
|
105
|
+
..., description="List of discovered repos"
|
|
106
|
+
)
|
|
107
|
+
total: int = Field(..., description="Total repos found")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ValidateRepoRequest(BaseModel):
|
|
111
|
+
"""Schema for repo validation request."""
|
|
112
|
+
|
|
113
|
+
path: str = Field(..., description="Path to validate as git repository")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ValidateRepoResponse(BaseModel):
|
|
117
|
+
"""Schema for repo validation response."""
|
|
118
|
+
|
|
119
|
+
is_valid: bool = Field(..., description="Whether path is valid git repo")
|
|
120
|
+
path: str = Field(..., description="Normalized path")
|
|
121
|
+
error_message: str | None = Field(None, description="Error message if invalid")
|
|
122
|
+
metadata: DiscoveredRepoResponse | None = Field(
|
|
123
|
+
None, description="Repo metadata if valid"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ============================================================================
|
|
128
|
+
# BoardRepo schemas
|
|
129
|
+
# ============================================================================
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BoardRepoBase(BaseModel):
|
|
133
|
+
"""Base schema for BoardRepo junction."""
|
|
134
|
+
|
|
135
|
+
board_id: str = Field(..., description="Board UUID")
|
|
136
|
+
repo_id: str = Field(..., description="Repo UUID")
|
|
137
|
+
is_primary: bool = Field(False, description="Whether this is the primary repo")
|
|
138
|
+
custom_setup_script: str | None = Field(
|
|
139
|
+
None, description="Per-board setup script override"
|
|
140
|
+
)
|
|
141
|
+
custom_cleanup_script: str | None = Field(
|
|
142
|
+
None, description="Per-board cleanup script override"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class BoardRepoCreate(BaseModel):
|
|
147
|
+
"""Schema for adding a repo to a board."""
|
|
148
|
+
|
|
149
|
+
repo_id: str = Field(..., description="Repo UUID to add")
|
|
150
|
+
is_primary: bool = Field(False, description="Whether this is the primary repo")
|
|
151
|
+
custom_setup_script: str | None = Field(
|
|
152
|
+
None, description="Per-board setup script override"
|
|
153
|
+
)
|
|
154
|
+
custom_cleanup_script: str | None = Field(
|
|
155
|
+
None, description="Per-board cleanup script override"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class BoardRepoUpdate(BaseModel):
|
|
160
|
+
"""Schema for updating board-repo association."""
|
|
161
|
+
|
|
162
|
+
is_primary: bool | None = Field(None, description="Set as primary repo")
|
|
163
|
+
custom_setup_script: str | None = Field(
|
|
164
|
+
None, description="Per-board setup script override"
|
|
165
|
+
)
|
|
166
|
+
custom_cleanup_script: str | None = Field(
|
|
167
|
+
None, description="Per-board cleanup script override"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class BoardRepoResponse(BoardRepoBase):
|
|
172
|
+
"""Schema for board-repo API responses."""
|
|
173
|
+
|
|
174
|
+
id: str = Field(..., description="BoardRepo UUID")
|
|
175
|
+
repo: RepoResponse = Field(..., description="Repo details")
|
|
176
|
+
created_at: datetime = Field(..., description="When association was created")
|
|
177
|
+
|
|
178
|
+
class Config:
|
|
179
|
+
from_attributes = True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class BoardRepoListResponse(BaseModel):
|
|
183
|
+
"""Schema for board repos list API response."""
|
|
184
|
+
|
|
185
|
+
board_id: str = Field(..., description="Board UUID")
|
|
186
|
+
repos: list[BoardRepoResponse] = Field(..., description="List of repos for board")
|
|
187
|
+
total: int = Field(..., description="Total number of repos")
|