draft-board 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. package/package.json +37 -0
@@ -0,0 +1,79 @@
1
+ """Board model - the primary permission and scoping boundary."""
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sqlalchemy import JSON, DateTime, ForeignKey, String, Text, func
7
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+
9
+ from app.models.base import Base
10
+
11
+ if TYPE_CHECKING:
12
+ from app.models.board_repo import BoardRepo
13
+ from app.models.goal import Goal
14
+ from app.models.job import Job
15
+ from app.models.ticket import Ticket
16
+ from app.models.user import User
17
+ from app.models.workspace import Workspace
18
+
19
+
20
+ class Board(Base):
21
+ """Board represents a project/repository boundary.
22
+
23
+ Key properties:
24
+ - Single repo per board (repo_root is authoritative)
25
+ - All goals, tickets, jobs, workspaces belong to a board
26
+ - board_id is the permission boundary for all operations
27
+
28
+ This prevents cross-tenant/cross-project data access.
29
+ """
30
+
31
+ __tablename__ = "boards"
32
+
33
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
34
+ name: Mapped[str] = mapped_column(String(255), nullable=False)
35
+ description: Mapped[str | None] = mapped_column(Text, nullable=True)
36
+
37
+ # The authoritative repo root for this board
38
+ # All file operations use this path - NOT client-provided paths
39
+ repo_root: Mapped[str] = mapped_column(String(1024), nullable=False)
40
+
41
+ # Optional: default branch for this repo
42
+ default_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
43
+
44
+ # Board-level configuration overrides (JSON)
45
+ # Overrides settings from draft.yaml
46
+ config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
47
+
48
+ # Owner (nullable for backward compat with single-user setups)
49
+ owner_id: Mapped[str | None] = mapped_column(
50
+ String(36),
51
+ ForeignKey("users.id", ondelete="CASCADE"),
52
+ nullable=True,
53
+ index=True,
54
+ )
55
+
56
+ created_at: Mapped[datetime] = mapped_column(
57
+ DateTime(timezone=True), server_default=func.now()
58
+ )
59
+ updated_at: Mapped[datetime] = mapped_column(
60
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
61
+ )
62
+
63
+ # Relationships
64
+ owner: Mapped["User | None"] = relationship("User", back_populates="boards")
65
+ goals: Mapped[list["Goal"]] = relationship(
66
+ "Goal", back_populates="board", cascade="all, delete-orphan"
67
+ )
68
+ tickets: Mapped[list["Ticket"]] = relationship(
69
+ "Ticket", back_populates="board", cascade="all, delete-orphan"
70
+ )
71
+ jobs: Mapped[list["Job"]] = relationship(
72
+ "Job", back_populates="board", cascade="all, delete-orphan"
73
+ )
74
+ workspaces: Mapped[list["Workspace"]] = relationship(
75
+ "Workspace", back_populates="board", cascade="all, delete-orphan"
76
+ )
77
+ board_repos: Mapped[list["BoardRepo"]] = relationship(
78
+ "BoardRepo", back_populates="board", cascade="all, delete-orphan"
79
+ )
@@ -0,0 +1,68 @@
1
+ """BoardRepo junction model - Board <-> Repo many-to-many relationship."""
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sqlalchemy import (
7
+ Boolean,
8
+ DateTime,
9
+ ForeignKey,
10
+ String,
11
+ Text,
12
+ UniqueConstraint,
13
+ func,
14
+ )
15
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
16
+
17
+ from app.models.base import Base
18
+
19
+ if TYPE_CHECKING:
20
+ from app.models.board import Board
21
+ from app.models.repo import Repo
22
+
23
+
24
+ class BoardRepo(Base):
25
+ """Junction table linking Boards to Repos (many-to-many).
26
+
27
+ Allows a board to have multiple repositories and a repo to be
28
+ shared across multiple boards.
29
+
30
+ Key properties:
31
+ - board_id + repo_id must be unique (one entry per board-repo pair)
32
+ - is_primary marks the primary repo for a board (used as default for operations)
33
+ - custom_setup_script allows per-board repo configuration overrides
34
+ """
35
+
36
+ __tablename__ = "board_repos"
37
+
38
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
39
+
40
+ # Foreign keys
41
+ board_id: Mapped[str] = mapped_column(
42
+ ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True
43
+ )
44
+ repo_id: Mapped[str] = mapped_column(
45
+ ForeignKey("repos.id", ondelete="CASCADE"), nullable=False, index=True
46
+ )
47
+
48
+ # Primary repo flag - each board should have exactly one primary repo
49
+ is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
50
+
51
+ # Per-board repo overrides (optional)
52
+ custom_setup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
53
+ custom_cleanup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
54
+
55
+ # Timestamps
56
+ created_at: Mapped[datetime] = mapped_column(
57
+ DateTime(timezone=True), server_default=func.now()
58
+ )
59
+
60
+ # Relationships
61
+ board: Mapped["Board"] = relationship("Board", back_populates="board_repos")
62
+ repo: Mapped["Repo"] = relationship("Repo", back_populates="board_repos")
63
+
64
+ # Constraints
65
+ __table_args__ = (UniqueConstraint("board_id", "repo_id", name="uq_board_repo"),)
66
+
67
+ def __repr__(self) -> str:
68
+ return f"<BoardRepo(board_id={self.board_id}, repo_id={self.repo_id}, is_primary={self.is_primary})>"
@@ -0,0 +1,42 @@
1
+ """Cost budget model for tracking spending limits."""
2
+
3
+ from datetime import datetime
4
+ from uuid import uuid4
5
+
6
+ from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, String
7
+ from sqlalchemy.orm import relationship
8
+
9
+ from app.models.base import Base
10
+
11
+
12
+ class CostBudget(Base):
13
+ """Budget configuration for cost tracking."""
14
+
15
+ __tablename__ = "cost_budgets"
16
+
17
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
18
+ goal_id = Column(
19
+ String(36),
20
+ ForeignKey("goals.id", ondelete="CASCADE"),
21
+ nullable=True,
22
+ index=True,
23
+ )
24
+
25
+ # Budget limits (None = unlimited)
26
+ daily_budget = Column(Float, nullable=True)
27
+ weekly_budget = Column(Float, nullable=True)
28
+ monthly_budget = Column(Float, nullable=True)
29
+ total_budget = Column(Float, nullable=True)
30
+
31
+ # Alert settings
32
+ warning_threshold = Column(Float, default=0.8, nullable=False) # 80% by default
33
+ pause_on_exceed = Column(Boolean, default=False, nullable=False)
34
+
35
+ # Timestamps
36
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
37
+ updated_at = Column(
38
+ DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
39
+ )
40
+
41
+ # Relationships
42
+ goal = relationship("Goal", back_populates="budget")
@@ -0,0 +1,40 @@
1
+ """Shared enumerations for Draft models.
2
+
3
+ This file contains event types and other enums that are NOT part of the
4
+ ticket state machine. The state machine (TicketState, transitions) lives
5
+ in state_machine.py. Event types are audit log entries, not state rules.
6
+ """
7
+
8
+ from enum import StrEnum
9
+
10
+
11
+ class EventType(StrEnum):
12
+ """Enum representing types of ticket events.
13
+
14
+ These are audit log event types, NOT state transition rules.
15
+ State transitions are governed by state_machine.py.
16
+ """
17
+
18
+ # Core lifecycle events
19
+ CREATED = "created"
20
+ TRANSITIONED = "transitioned"
21
+ UPDATED = "updated"
22
+ COMMENT = "comment"
23
+
24
+ # Merge lifecycle events
25
+ MERGE_REQUESTED = "merge_requested"
26
+ MERGE_SUCCEEDED = "merge_succeeded"
27
+ MERGE_FAILED = "merge_failed"
28
+
29
+ # Cleanup events (distinct types for clear analytics/UX)
30
+ WORKTREE_CLEANED = "worktree_cleaned"
31
+ WORKTREE_CLEANUP_FAILED = "worktree_cleanup_failed"
32
+
33
+
34
+ class ActorType(StrEnum):
35
+ """Enum representing who performed an action."""
36
+
37
+ HUMAN = "human"
38
+ PLANNER = "planner"
39
+ SYSTEM = "system"
40
+ EXECUTOR = "executor"
@@ -0,0 +1,132 @@
1
+ """Evidence model for storing verification command results."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from enum import StrEnum
6
+ from typing import TYPE_CHECKING
7
+
8
+ from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
9
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
10
+
11
+ from app.models.base import Base
12
+
13
+ if TYPE_CHECKING:
14
+ from app.models.job import Job
15
+ from app.models.ticket import Ticket
16
+
17
+
18
+ class EvidenceKind(StrEnum):
19
+ """Enum representing the kind of evidence.
20
+
21
+ Metadata evidence (JSON in stdout_path):
22
+ - EXECUTOR_META: JSON with exit_code, duration, executor_type, mode, command
23
+ - VERIFY_META: JSON with exit_code, commands_run, duration, results per command
24
+
25
+ Evidence types for execution:
26
+ - EXECUTOR_STDOUT: stdout from executor CLI (Claude/Cursor)
27
+ - EXECUTOR_STDERR: stderr from executor CLI
28
+ - GIT_DIFF_STAT: output of `git diff --stat`
29
+ - GIT_DIFF_PATCH: full git diff patch
30
+
31
+ Evidence types for verification:
32
+ - VERIFY_STDOUT: stdout from verification command
33
+ - VERIFY_STDERR: stderr from verification command
34
+
35
+ Legacy types (kept for backwards compatibility):
36
+ - COMMAND_LOG: generic command output
37
+ - TEST_REPORT: test framework report
38
+ """
39
+
40
+ # Metadata evidence (JSON)
41
+ EXECUTOR_META = (
42
+ "executor_meta" # JSON: exit_code, duration_ms, executor_type, mode, command
43
+ )
44
+ VERIFY_META = "verify_meta" # JSON: exit_code, commands, duration_ms, results
45
+
46
+ # Executor evidence
47
+ EXECUTOR_STDOUT = "executor_stdout"
48
+ EXECUTOR_STDERR = "executor_stderr"
49
+
50
+ # Git diff evidence
51
+ GIT_DIFF_STAT = "git_diff_stat"
52
+ GIT_DIFF_PATCH = "git_diff_patch"
53
+
54
+ # Verification evidence
55
+ VERIFY_STDOUT = "verify_stdout"
56
+ VERIFY_STDERR = "verify_stderr"
57
+
58
+ # Merge evidence
59
+ MERGE_STDOUT = "merge_stdout"
60
+ MERGE_STDERR = "merge_stderr"
61
+ MERGE_META = "merge_meta" # JSON: strategy, branch, base_branch, exit_code, duration_ms, evidence_ids
62
+
63
+ # Legacy types (backwards compatibility)
64
+ COMMAND_LOG = "command_log"
65
+ TEST_REPORT = "test_report"
66
+
67
+
68
+ class Evidence(Base):
69
+ """Evidence model representing verification command execution results."""
70
+
71
+ __tablename__ = "evidence"
72
+
73
+ id: Mapped[str] = mapped_column(
74
+ String(36),
75
+ primary_key=True,
76
+ default=lambda: str(uuid.uuid4()),
77
+ )
78
+ ticket_id: Mapped[str] = mapped_column(
79
+ String(36),
80
+ ForeignKey("tickets.id", ondelete="CASCADE"),
81
+ nullable=False,
82
+ index=True,
83
+ )
84
+ job_id: Mapped[str] = mapped_column(
85
+ String(36),
86
+ ForeignKey("jobs.id", ondelete="CASCADE"),
87
+ nullable=False,
88
+ index=True,
89
+ )
90
+ kind: Mapped[str] = mapped_column(
91
+ String(50),
92
+ nullable=False,
93
+ default=EvidenceKind.COMMAND_LOG.value,
94
+ )
95
+ command: Mapped[str] = mapped_column(
96
+ Text,
97
+ nullable=False,
98
+ )
99
+ exit_code: Mapped[int] = mapped_column(
100
+ Integer,
101
+ nullable=False,
102
+ )
103
+ stdout_path: Mapped[str | None] = mapped_column(
104
+ Text,
105
+ nullable=True,
106
+ )
107
+ stderr_path: Mapped[str | None] = mapped_column(
108
+ Text,
109
+ nullable=True,
110
+ )
111
+ created_at: Mapped[datetime] = mapped_column(
112
+ DateTime,
113
+ server_default=func.now(),
114
+ nullable=False,
115
+ )
116
+
117
+ # Relationships
118
+ ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="evidence")
119
+ job: Mapped["Job"] = relationship("Job", back_populates="evidence")
120
+
121
+ @property
122
+ def kind_enum(self) -> EvidenceKind:
123
+ """Get the kind as an EvidenceKind enum."""
124
+ return EvidenceKind(self.kind)
125
+
126
+ @property
127
+ def succeeded(self) -> bool:
128
+ """Check if the command succeeded (exit code 0)."""
129
+ return self.exit_code == 0
130
+
131
+ def __repr__(self) -> str:
132
+ return f"<Evidence(id={self.id}, command={self.command[:30]}..., exit_code={self.exit_code})>"
@@ -0,0 +1,102 @@
1
+ """Goal model for Draft."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
9
+
10
+ from app.models.base import Base
11
+
12
+ if TYPE_CHECKING:
13
+ from app.models.agent_conversation_history import AgentConversationHistory
14
+ from app.models.board import Board
15
+ from app.models.cost_budget import CostBudget
16
+ from app.models.merge_checklist import MergeChecklist
17
+ from app.models.ticket import Ticket
18
+
19
+
20
+ class Goal(Base):
21
+ """Goal model representing a high-level objective.
22
+
23
+ IMPORTANT: Goals are scoped by board_id for permission enforcement.
24
+ """
25
+
26
+ __tablename__ = "goals"
27
+
28
+ id: Mapped[str] = mapped_column(
29
+ String(36),
30
+ primary_key=True,
31
+ default=lambda: str(uuid.uuid4()),
32
+ )
33
+ board_id: Mapped[str | None] = mapped_column(
34
+ String(36),
35
+ ForeignKey("boards.id", ondelete="CASCADE"),
36
+ nullable=True, # Nullable for migration compatibility
37
+ index=True,
38
+ )
39
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
40
+ description: Mapped[str | None] = mapped_column(Text, nullable=True)
41
+ created_at: Mapped[datetime] = mapped_column(
42
+ DateTime,
43
+ server_default=func.now(),
44
+ nullable=False,
45
+ )
46
+ updated_at: Mapped[datetime] = mapped_column(
47
+ DateTime,
48
+ server_default=func.now(),
49
+ onupdate=func.now(),
50
+ nullable=False,
51
+ )
52
+
53
+ # Autonomy fields
54
+ autonomy_enabled: Mapped[bool] = mapped_column(
55
+ Boolean, default=False, server_default="0", nullable=False
56
+ )
57
+ auto_approve_tickets: Mapped[bool] = mapped_column(
58
+ Boolean, default=False, server_default="0", nullable=False
59
+ )
60
+ auto_approve_revisions: Mapped[bool] = mapped_column(
61
+ Boolean, default=False, server_default="0", nullable=False
62
+ )
63
+ auto_merge: Mapped[bool] = mapped_column(
64
+ Boolean, default=False, server_default="0", nullable=False
65
+ )
66
+ auto_approve_followups: Mapped[bool] = mapped_column(
67
+ Boolean, default=False, server_default="0", nullable=False
68
+ )
69
+ max_auto_approvals: Mapped[int | None] = mapped_column(
70
+ Integer, default=None, nullable=True
71
+ )
72
+ auto_approval_count: Mapped[int] = mapped_column(
73
+ Integer, default=0, server_default="0", nullable=False
74
+ )
75
+
76
+ # Relationships
77
+ board: Mapped["Board | None"] = relationship("Board", back_populates="goals")
78
+ tickets: Mapped[list["Ticket"]] = relationship(
79
+ "Ticket",
80
+ back_populates="goal",
81
+ cascade="all, delete-orphan",
82
+ )
83
+ budget: Mapped["CostBudget | None"] = relationship(
84
+ "CostBudget",
85
+ back_populates="goal",
86
+ uselist=False,
87
+ cascade="all, delete-orphan",
88
+ )
89
+ merge_checklist: Mapped["MergeChecklist | None"] = relationship(
90
+ "MergeChecklist",
91
+ back_populates="goal",
92
+ uselist=False,
93
+ cascade="all, delete-orphan",
94
+ )
95
+ agent_conversation_history: Mapped[list["AgentConversationHistory"]] = relationship(
96
+ "AgentConversationHistory",
97
+ back_populates="goal",
98
+ cascade="all, delete-orphan",
99
+ )
100
+
101
+ def __repr__(self) -> str:
102
+ return f"<Goal(id={self.id}, title={self.title})>"
@@ -0,0 +1,30 @@
1
+ """SQLite-backed idempotency cache model (replaces Redis SETNX)."""
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import DateTime, String, Text, func
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from app.models.base import Base
9
+
10
+
11
+ class IdempotencyEntry(Base):
12
+ """Idempotency lock and result cache entry.
13
+
14
+ Replaces Redis SETNX + result cache. Lock acquisition is atomic
15
+ via INSERT OR IGNORE (rowcount == 1 means acquired).
16
+ """
17
+
18
+ __tablename__ = "idempotency_cache"
19
+
20
+ cache_key: Mapped[str] = mapped_column(String(512), primary_key=True)
21
+ lock_value: Mapped[str | None] = mapped_column(Text, nullable=True)
22
+ result_value: Mapped[str | None] = mapped_column(Text, nullable=True)
23
+ lock_expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
24
+ result_expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
25
+ created_at: Mapped[datetime] = mapped_column(
26
+ DateTime, nullable=False, server_default=func.now()
27
+ )
28
+
29
+ def __repr__(self) -> str:
30
+ return f"<IdempotencyEntry(key={self.cache_key[:30]}...)>"
@@ -0,0 +1,163 @@
1
+ """Job model for tracking long-running task executions."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from enum import StrEnum
6
+ from typing import TYPE_CHECKING
7
+
8
+ from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
9
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
10
+
11
+ from app.models.base import Base
12
+
13
+ if TYPE_CHECKING:
14
+ from app.models.board import Board
15
+ from app.models.evidence import Evidence
16
+ from app.models.normalized_log import NormalizedLogEntry
17
+ from app.models.revision import Revision
18
+ from app.models.ticket import Ticket
19
+
20
+
21
+ class JobKind(StrEnum):
22
+ """Enum representing the kind of job."""
23
+
24
+ EXECUTE = "execute"
25
+ VERIFY = "verify"
26
+ RESUME = "resume" # Resume after interactive (human) completion
27
+
28
+
29
+ class JobStatus(StrEnum):
30
+ """Enum representing the status of a job."""
31
+
32
+ QUEUED = "queued"
33
+ RUNNING = "running"
34
+ SUCCEEDED = "succeeded"
35
+ FAILED = "failed"
36
+ CANCELED = "canceled"
37
+
38
+
39
+ class Job(Base):
40
+ """Job model representing a long-running task execution.
41
+
42
+ IMPORTANT: Jobs are scoped by board_id for permission enforcement.
43
+ """
44
+
45
+ __tablename__ = "jobs"
46
+
47
+ id: Mapped[str] = mapped_column(
48
+ String(36),
49
+ primary_key=True,
50
+ default=lambda: str(uuid.uuid4()),
51
+ )
52
+ board_id: Mapped[str | None] = mapped_column(
53
+ String(36),
54
+ ForeignKey("boards.id", ondelete="CASCADE"),
55
+ nullable=True, # Nullable for migration compatibility
56
+ index=True,
57
+ )
58
+ ticket_id: Mapped[str] = mapped_column(
59
+ String(36),
60
+ ForeignKey("tickets.id", ondelete="CASCADE"),
61
+ nullable=False,
62
+ index=True,
63
+ )
64
+ kind: Mapped[str] = mapped_column(
65
+ String(20),
66
+ nullable=False,
67
+ )
68
+ status: Mapped[str] = mapped_column(
69
+ String(20),
70
+ default=JobStatus.QUEUED.value,
71
+ nullable=False,
72
+ index=True,
73
+ )
74
+ created_at: Mapped[datetime] = mapped_column(
75
+ DateTime,
76
+ server_default=func.now(),
77
+ nullable=False,
78
+ )
79
+ started_at: Mapped[datetime | None] = mapped_column(
80
+ DateTime,
81
+ nullable=True,
82
+ )
83
+ finished_at: Mapped[datetime | None] = mapped_column(
84
+ DateTime,
85
+ nullable=True,
86
+ )
87
+ exit_code: Mapped[int | None] = mapped_column(
88
+ Integer,
89
+ nullable=True,
90
+ )
91
+ log_path: Mapped[str | None] = mapped_column(
92
+ Text,
93
+ nullable=True,
94
+ )
95
+ celery_task_id: Mapped[str | None] = mapped_column(
96
+ String(255),
97
+ nullable=True,
98
+ )
99
+ # For jobs triggered by review feedback, tracks which revision is being addressed
100
+ source_revision_id: Mapped[str | None] = mapped_column(
101
+ String(36),
102
+ ForeignKey("revisions.id", ondelete="SET NULL"),
103
+ nullable=True,
104
+ index=True,
105
+ )
106
+ # Session ID for executor session resume (e.g. Claude --resume)
107
+ session_id: Mapped[str | None] = mapped_column(
108
+ String(255),
109
+ nullable=True,
110
+ )
111
+ # Health monitoring fields
112
+ last_heartbeat_at: Mapped[datetime | None] = mapped_column(
113
+ DateTime,
114
+ nullable=True,
115
+ index=True,
116
+ )
117
+ timeout_seconds: Mapped[int | None] = mapped_column(
118
+ Integer,
119
+ nullable=True,
120
+ )
121
+
122
+ # Relationships
123
+ board: Mapped["Board | None"] = relationship("Board", back_populates="jobs")
124
+ ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="jobs")
125
+ evidence: Mapped[list["Evidence"]] = relationship(
126
+ "Evidence",
127
+ back_populates="job",
128
+ cascade="all, delete-orphan",
129
+ order_by="Evidence.created_at.desc()",
130
+ )
131
+ # The revision created by this job (via Revision.job_id -> Job.id)
132
+ revision: Mapped["Revision | None"] = relationship(
133
+ "Revision",
134
+ back_populates="job",
135
+ uselist=False,
136
+ foreign_keys="Revision.job_id",
137
+ )
138
+ # For jobs triggered by review, the revision being addressed
139
+ source_revision: Mapped["Revision | None"] = relationship(
140
+ "Revision",
141
+ foreign_keys="Job.source_revision_id",
142
+ viewonly=True, # Don't allow writes through this relationship
143
+ )
144
+ # Normalized log entries for this job
145
+ normalized_logs: Mapped[list["NormalizedLogEntry"]] = relationship(
146
+ "NormalizedLogEntry",
147
+ back_populates="job",
148
+ cascade="all, delete-orphan",
149
+ order_by="NormalizedLogEntry.sequence",
150
+ )
151
+
152
+ @property
153
+ def kind_enum(self) -> JobKind:
154
+ """Get the kind as a JobKind enum."""
155
+ return JobKind(self.kind)
156
+
157
+ @property
158
+ def status_enum(self) -> JobStatus:
159
+ """Get the status as a JobStatus enum."""
160
+ return JobStatus(self.status)
161
+
162
+ def __repr__(self) -> str:
163
+ return f"<Job(id={self.id}, kind={self.kind}, status={self.status})>"