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,39 @@
1
+ """SQLite-backed job queue model (replaces Celery/Redis broker)."""
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import DateTime, Index, Integer, String, Text, func
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from app.models.base import Base
9
+
10
+
11
+ class JobQueueEntry(Base):
12
+ """A queued task for the SQLite worker to process.
13
+
14
+ Replaces the Celery broker queue. Tasks are claimed atomically
15
+ via UPDATE...WHERE status='pending' ORDER BY priority DESC, created_at ASC.
16
+ """
17
+
18
+ __tablename__ = "job_queue"
19
+ __table_args__ = (
20
+ Index("ix_job_queue_claim_order", "status", "priority", "created_at"),
21
+ )
22
+
23
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
24
+ task_name: Mapped[str] = mapped_column(String(255), nullable=False)
25
+ args_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
26
+ status: Mapped[str] = mapped_column(
27
+ String(20), nullable=False, default="pending", index=True
28
+ )
29
+ claimed_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
30
+ claimed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
31
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
32
+ result_json: Mapped[str | None] = mapped_column(Text, nullable=True)
33
+ priority: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
34
+ created_at: Mapped[datetime] = mapped_column(
35
+ DateTime, nullable=False, server_default=func.now()
36
+ )
37
+
38
+ def __repr__(self) -> str:
39
+ return f"<JobQueueEntry(id={self.id}, task={self.task_name}, status={self.status})>"
@@ -0,0 +1,28 @@
1
+ """SQLite-backed key-value store model (replaces Redis GET/SET)."""
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 KVStoreEntry(Base):
12
+ """Generic key-value store with optional TTL.
13
+
14
+ Replaces Redis GET/SET for queued messages and follow-up prompts.
15
+ Expired entries are cleaned up inline on access.
16
+ """
17
+
18
+ __tablename__ = "kv_store"
19
+
20
+ key: Mapped[str] = mapped_column(String(512), primary_key=True)
21
+ value: Mapped[str] = mapped_column(Text, nullable=False)
22
+ expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
23
+ created_at: Mapped[datetime] = mapped_column(
24
+ DateTime, nullable=False, server_default=func.now()
25
+ )
26
+
27
+ def __repr__(self) -> str:
28
+ return f"<KVStoreEntry(key={self.key[:30]}...)>"
@@ -0,0 +1,87 @@
1
+ """Merge checklist model for tracking merge readiness."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sqlalchemy import Boolean, DateTime, Float, 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.goal import Goal
14
+
15
+
16
+ class MergeChecklist(Base):
17
+ """Tracks merge readiness for a goal's tickets.
18
+
19
+ Combines automatic checks (tests, cost) and manual checks (review, security).
20
+ """
21
+
22
+ __tablename__ = "merge_checklists"
23
+
24
+ id: Mapped[str] = mapped_column(
25
+ String(36), primary_key=True, default=lambda: str(uuid.uuid4())
26
+ )
27
+ goal_id: Mapped[str] = mapped_column(
28
+ String(36),
29
+ ForeignKey("goals.id", ondelete="CASCADE"),
30
+ nullable=False,
31
+ index=True,
32
+ )
33
+
34
+ # Auto-checks (computed from system state)
35
+ all_tests_passed: Mapped[bool] = mapped_column(Boolean, default=False)
36
+ total_files_changed: Mapped[int] = mapped_column(Integer, default=0)
37
+ total_lines_changed: Mapped[int] = mapped_column(Integer, default=0)
38
+ total_cost_usd: Mapped[float | None] = mapped_column(Float, nullable=True)
39
+ budget_exceeded: Mapped[bool] = mapped_column(Boolean, default=False)
40
+
41
+ # Manual checks (require human confirmation)
42
+ code_reviewed: Mapped[bool] = mapped_column(Boolean, default=False)
43
+ no_sensitive_data: Mapped[bool] = mapped_column(Boolean, default=False)
44
+ rollback_plan_understood: Mapped[bool] = mapped_column(Boolean, default=False)
45
+ documentation_updated: Mapped[bool] = mapped_column(Boolean, default=False)
46
+
47
+ # Rollback plan
48
+ rollback_plan_json: Mapped[str | None] = mapped_column(Text, nullable=True)
49
+ risk_level: Mapped[str] = mapped_column(
50
+ String(20),
51
+ default="low", # low, medium, high
52
+ )
53
+
54
+ # Status
55
+ ready_to_merge: Mapped[bool] = mapped_column(Boolean, default=False)
56
+ merged_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
57
+
58
+ created_at: Mapped[datetime] = mapped_column(
59
+ DateTime, server_default=func.now(), nullable=False
60
+ )
61
+ updated_at: Mapped[datetime] = mapped_column(
62
+ DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
63
+ )
64
+
65
+ # Relationships
66
+ goal: Mapped["Goal"] = relationship("Goal", back_populates="merge_checklist")
67
+
68
+ def __repr__(self) -> str:
69
+ return f"<MergeChecklist(id={self.id}, goal_id={self.goal_id}, ready={self.ready_to_merge})>"
70
+
71
+ def is_ready_to_merge(self) -> bool:
72
+ """Check if all conditions are met for merging."""
73
+ # All auto-checks must pass
74
+ if not self.all_tests_passed:
75
+ return False
76
+ if self.budget_exceeded:
77
+ return False
78
+
79
+ # All manual checks must be confirmed
80
+ if not (
81
+ self.code_reviewed
82
+ and self.no_sensitive_data
83
+ and self.rollback_plan_understood
84
+ ):
85
+ return False
86
+
87
+ return True
@@ -0,0 +1,100 @@
1
+ """Database models for normalized log entries."""
2
+
3
+ import enum
4
+ from datetime import datetime
5
+
6
+ from sqlalchemy import (
7
+ JSON,
8
+ Boolean,
9
+ Column,
10
+ DateTime,
11
+ Enum,
12
+ ForeignKey,
13
+ Index,
14
+ Integer,
15
+ String,
16
+ Text,
17
+ )
18
+ from sqlalchemy.orm import relationship
19
+
20
+ from app.models.base import Base
21
+
22
+
23
+ class LogEntryType(enum.StrEnum):
24
+ """Semantic types for normalized log entries."""
25
+
26
+ THINKING = "thinking"
27
+ ASSISTANT_MESSAGE = "assistant_message" # Agent's response/reasoning
28
+ FILE_EDIT = "file_edit"
29
+ FILE_CREATE = "file_create"
30
+ FILE_DELETE = "file_delete"
31
+ COMMAND_RUN = "command_run"
32
+ TOOL_CALL = "tool_call"
33
+ ERROR = "error"
34
+ USER_MESSAGE = "user_message"
35
+ SYSTEM_MESSAGE = "system_message"
36
+ LOADING = "loading"
37
+ TODO_LIST = "todo_list" # Agent's todo/task list
38
+
39
+ # UDAR agent entry types
40
+ AGENT_UNDERSTANDING = "agent_understanding" # Context gathered in Understand phase
41
+ AGENT_DECISION = "agent_decision" # LLM reasoning in Decide phase
42
+ AGENT_VALIDATION = "agent_validation" # Validation results in Validate phase
43
+ AGENT_TOOL_CALL = "agent_tool_call" # Tool invocations (deterministic)
44
+
45
+
46
+ class NormalizedLogEntry(Base):
47
+ """Structured, semantic log entry parsed from raw agent output."""
48
+
49
+ __tablename__ = "normalized_log_entries"
50
+
51
+ id = Column(
52
+ String(36), primary_key=True, default=lambda: str(__import__("uuid").uuid4())
53
+ )
54
+
55
+ # Foreign key to job
56
+ job_id = Column(String, ForeignKey("jobs.id"), nullable=False, index=True)
57
+ job = relationship("Job", back_populates="normalized_logs")
58
+
59
+ # Sequence number for ordering
60
+ sequence = Column(Integer, nullable=False)
61
+
62
+ # Timestamp of the entry
63
+ timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
64
+
65
+ # Entry type (semantic category)
66
+ entry_type = Column(Enum(LogEntryType), nullable=False, index=True)
67
+
68
+ # Text content (can be markdown, code, etc.)
69
+ content = Column(Text, nullable=False)
70
+
71
+ # Structured metadata (JSON)
72
+ # Examples:
73
+ # - file_edit: {"file_path": "app/auth.py", "diff": "...", "language": "python"}
74
+ # - command_run: {"command": "pytest", "exit_code": 0, "output": "..."}
75
+ # - tool_call: {"tool_name": "web_search", "args": {...}, "result": {...}}
76
+ # - error: {"error_type": "SyntaxError", "traceback": "..."}
77
+ # Note: "metadata" is reserved in SQLAlchemy, so using "entry_metadata"
78
+ entry_metadata = Column(JSON, nullable=True)
79
+
80
+ # Display flags
81
+ collapsed = Column(Boolean, default=False) # Start collapsed?
82
+ highlight = Column(Boolean, default=False) # Highlight in UI?
83
+
84
+ __table_args__ = (
85
+ Index("ix_normalized_log_entries_job_sequence", "job_id", "sequence"),
86
+ )
87
+
88
+ def to_dict(self):
89
+ """Convert to dictionary for API responses."""
90
+ return {
91
+ "id": self.id,
92
+ "job_id": self.job_id,
93
+ "sequence": self.sequence,
94
+ "timestamp": self.timestamp.isoformat() if self.timestamp else None,
95
+ "entry_type": self.entry_type.value if self.entry_type else None,
96
+ "content": self.content,
97
+ "metadata": self.entry_metadata or {}, # Return as "metadata" for frontend
98
+ "collapsed": self.collapsed,
99
+ "highlight": self.highlight,
100
+ }
@@ -0,0 +1,43 @@
1
+ """Planner lock model for ensuring single-tick execution."""
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import DateTime, String, func
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from app.models.base import Base
9
+
10
+
11
+ class PlannerLock(Base):
12
+ """Single-row lock table to prevent concurrent planner ticks.
13
+
14
+ This ensures that only one planner tick can run at a time,
15
+ preventing race conditions where two ticks might both see
16
+ "no executing ticket" and both enqueue jobs.
17
+
18
+ Usage:
19
+ - Acquire: INSERT with lock_key="planner_tick"
20
+ - Release: DELETE the row
21
+ - Check: SELECT to see if lock is held
22
+ """
23
+
24
+ __tablename__ = "planner_locks"
25
+
26
+ # Fixed key for the planner lock
27
+ lock_key: Mapped[str] = mapped_column(
28
+ String(50),
29
+ primary_key=True,
30
+ default="planner_tick",
31
+ )
32
+ acquired_at: Mapped[datetime] = mapped_column(
33
+ DateTime,
34
+ server_default=func.now(),
35
+ nullable=False,
36
+ )
37
+ owner_id: Mapped[str | None] = mapped_column(
38
+ String(255),
39
+ nullable=True,
40
+ )
41
+
42
+ def __repr__(self) -> str:
43
+ return f"<PlannerLock(key={self.lock_key}, acquired={self.acquired_at})>"
@@ -0,0 +1,25 @@
1
+ """SQLite-backed rate limit entry model (replaces Redis sorted sets)."""
2
+
3
+ from sqlalchemy import Float, Integer, String
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+
6
+ from app.models.base import Base
7
+
8
+
9
+ class RateLimitEntry(Base):
10
+ """A rate limit cost entry.
11
+
12
+ Replaces Redis sorted sets. Entries are cleaned up inline
13
+ (DELETE WHERE expires_at < now) before checking budget.
14
+ """
15
+
16
+ __tablename__ = "rate_limit_entries"
17
+
18
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
19
+ client_key: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
20
+ cost: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
21
+ recorded_at: Mapped[float] = mapped_column(Float, nullable=False)
22
+ expires_at: Mapped[float] = mapped_column(Float, nullable=False, index=True)
23
+
24
+ def __repr__(self) -> str:
25
+ return f"<RateLimitEntry(key={self.client_key}, cost={self.cost})>"
@@ -0,0 +1,66 @@
1
+ """Repo model - global repository registry."""
2
+
3
+ from datetime import datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sqlalchemy import DateTime, 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
+
14
+
15
+ class Repo(Base):
16
+ """Global repository registry.
17
+
18
+ Each Repo represents a git repository on the filesystem.
19
+ Repos can be associated with multiple boards via BoardRepo junction table.
20
+
21
+ Key properties:
22
+ - path is unique - only one Repo per filesystem path
23
+ - display_name is user-friendly name for UI
24
+ - Optional scripts for setup/cleanup/dev server
25
+ - Git metadata (default_branch, remote_url) cached for performance
26
+ """
27
+
28
+ __tablename__ = "repos"
29
+
30
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
31
+
32
+ # Filesystem path to git repository (unique)
33
+ path: Mapped[str] = mapped_column(
34
+ String(1024), unique=True, nullable=False, index=True
35
+ )
36
+
37
+ # Repository name (derived from path, e.g., "my-project")
38
+ name: Mapped[str] = mapped_column(String(255), nullable=False)
39
+
40
+ # User-friendly display name (editable)
41
+ display_name: Mapped[str] = mapped_column(String(255), nullable=False)
42
+
43
+ # Optional per-repo scripts
44
+ setup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
45
+ cleanup_script: Mapped[str | None] = mapped_column(Text, nullable=True)
46
+ dev_server_script: Mapped[str | None] = mapped_column(Text, nullable=True)
47
+
48
+ # Git metadata (cached)
49
+ default_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
50
+ remote_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
51
+
52
+ # Timestamps
53
+ created_at: Mapped[datetime] = mapped_column(
54
+ DateTime(timezone=True), server_default=func.now()
55
+ )
56
+ updated_at: Mapped[datetime] = mapped_column(
57
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
58
+ )
59
+
60
+ # Relationships
61
+ board_repos: Mapped[list["BoardRepo"]] = relationship(
62
+ "BoardRepo", back_populates="repo", cascade="all, delete-orphan"
63
+ )
64
+
65
+ def __repr__(self) -> str:
66
+ return f"<Repo(id={self.id}, name={self.name}, path={self.path})>"
@@ -0,0 +1,91 @@
1
+ """ReviewComment model for inline comments on revision diffs."""
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 Boolean, 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.revision import Revision
15
+
16
+
17
+ class AuthorType(StrEnum):
18
+ """Enum representing the type of author for a review comment."""
19
+
20
+ HUMAN = "human"
21
+ AGENT = "agent"
22
+ SYSTEM = "system"
23
+
24
+
25
+ class ReviewComment(Base):
26
+ """ReviewComment model representing inline comments on a revision diff.
27
+
28
+ Comments are anchored using a sha1 hash of (file_path + hunk_header + line_content)
29
+ to survive small line shifts between revisions.
30
+ """
31
+
32
+ __tablename__ = "review_comments"
33
+
34
+ id: Mapped[str] = mapped_column(
35
+ String(36),
36
+ primary_key=True,
37
+ default=lambda: str(uuid.uuid4()),
38
+ )
39
+ revision_id: Mapped[str] = mapped_column(
40
+ String(36),
41
+ ForeignKey("revisions.id", ondelete="CASCADE"),
42
+ nullable=False,
43
+ index=True,
44
+ )
45
+ file_path: Mapped[str] = mapped_column(
46
+ String(500),
47
+ nullable=False,
48
+ )
49
+ line_number: Mapped[int] = mapped_column(
50
+ Integer,
51
+ nullable=False,
52
+ )
53
+ anchor: Mapped[str] = mapped_column(
54
+ String(40), # sha1 hex digest (truncated to 16 chars in practice)
55
+ nullable=False,
56
+ index=True,
57
+ )
58
+ line_content: Mapped[str | None] = mapped_column(
59
+ Text,
60
+ nullable=True,
61
+ )
62
+ body: Mapped[str] = mapped_column(
63
+ Text,
64
+ nullable=False,
65
+ )
66
+ author_type: Mapped[str] = mapped_column(
67
+ String(20),
68
+ default=AuthorType.HUMAN.value,
69
+ nullable=False,
70
+ )
71
+ resolved: Mapped[bool] = mapped_column(
72
+ Boolean,
73
+ default=False,
74
+ nullable=False,
75
+ )
76
+ created_at: Mapped[datetime] = mapped_column(
77
+ DateTime,
78
+ server_default=func.now(),
79
+ nullable=False,
80
+ )
81
+
82
+ # Relationships
83
+ revision: Mapped["Revision"] = relationship("Revision", back_populates="comments")
84
+
85
+ @property
86
+ def author_type_enum(self) -> AuthorType:
87
+ """Get the author_type as an AuthorType enum."""
88
+ return AuthorType(self.author_type)
89
+
90
+ def __repr__(self) -> str:
91
+ return f"<ReviewComment(id={self.id}, file={self.file_path}, line={self.line_number}, resolved={self.resolved})>"
@@ -0,0 +1,69 @@
1
+ """ReviewSummary model for overall review decisions on revisions."""
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, 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.revision import Revision
15
+
16
+
17
+ class ReviewDecision(StrEnum):
18
+ """Enum representing the review decision."""
19
+
20
+ APPROVED = "approved"
21
+ CHANGES_REQUESTED = "changes_requested"
22
+
23
+
24
+ class ReviewSummary(Base):
25
+ """ReviewSummary model representing the overall review decision for a revision.
26
+
27
+ Only one ReviewSummary per revision is allowed.
28
+ """
29
+
30
+ __tablename__ = "review_summaries"
31
+
32
+ id: Mapped[str] = mapped_column(
33
+ String(36),
34
+ primary_key=True,
35
+ default=lambda: str(uuid.uuid4()),
36
+ )
37
+ revision_id: Mapped[str] = mapped_column(
38
+ String(36),
39
+ ForeignKey("revisions.id", ondelete="CASCADE"),
40
+ nullable=False,
41
+ unique=True,
42
+ index=True,
43
+ )
44
+ decision: Mapped[str] = mapped_column(
45
+ String(30),
46
+ nullable=False,
47
+ )
48
+ body: Mapped[str] = mapped_column(
49
+ Text,
50
+ nullable=False,
51
+ )
52
+ created_at: Mapped[datetime] = mapped_column(
53
+ DateTime,
54
+ server_default=func.now(),
55
+ nullable=False,
56
+ )
57
+
58
+ # Relationships
59
+ revision: Mapped["Revision"] = relationship(
60
+ "Revision", back_populates="review_summary"
61
+ )
62
+
63
+ @property
64
+ def decision_enum(self) -> ReviewDecision:
65
+ """Get the decision as a ReviewDecision enum."""
66
+ return ReviewDecision(self.decision)
67
+
68
+ def __repr__(self) -> str:
69
+ return f"<ReviewSummary(id={self.id}, revision_id={self.revision_id}, decision={self.decision})>"
@@ -0,0 +1,130 @@
1
+ """Revision model for tracking agent code change iterations."""
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, UniqueConstraint, 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.evidence import Evidence
15
+ from app.models.job import Job
16
+ from app.models.review_comment import ReviewComment
17
+ from app.models.review_summary import ReviewSummary
18
+ from app.models.ticket import Ticket
19
+
20
+
21
+ class RevisionStatus(StrEnum):
22
+ """Enum representing the status of a revision."""
23
+
24
+ OPEN = "open"
25
+ CHANGES_REQUESTED = "changes_requested"
26
+ APPROVED = "approved"
27
+ SUPERSEDED = "superseded"
28
+
29
+
30
+ class Revision(Base):
31
+ """Revision model representing one agent iteration for a ticket.
32
+
33
+ A revision is similar to a GitHub PR snapshot - it contains:
34
+ - A diff (stat + patch as evidence)
35
+ - Review comments
36
+ - A final review decision
37
+
38
+ Only one revision per ticket can be 'open' at a time.
39
+ When a new revision is created, previous open revision becomes 'superseded'.
40
+ """
41
+
42
+ __tablename__ = "revisions"
43
+ __table_args__ = (
44
+ UniqueConstraint("ticket_id", "number", name="uq_revision_ticket_number"),
45
+ UniqueConstraint("ticket_id", "job_id", name="uq_revision_ticket_job"),
46
+ )
47
+
48
+ id: Mapped[str] = mapped_column(
49
+ String(36),
50
+ primary_key=True,
51
+ default=lambda: str(uuid.uuid4()),
52
+ )
53
+ ticket_id: Mapped[str] = mapped_column(
54
+ String(36),
55
+ ForeignKey("tickets.id", ondelete="CASCADE"),
56
+ nullable=False,
57
+ index=True,
58
+ )
59
+ job_id: Mapped[str] = mapped_column(
60
+ String(36),
61
+ ForeignKey("jobs.id", ondelete="CASCADE"),
62
+ nullable=False,
63
+ index=True,
64
+ )
65
+ number: Mapped[int] = mapped_column(
66
+ Integer,
67
+ nullable=False,
68
+ )
69
+ status: Mapped[str] = mapped_column(
70
+ String(50),
71
+ default=RevisionStatus.OPEN.value,
72
+ nullable=False,
73
+ index=True,
74
+ )
75
+ diff_stat_evidence_id: Mapped[str | None] = mapped_column(
76
+ String(36),
77
+ ForeignKey("evidence.id", ondelete="SET NULL"),
78
+ nullable=True,
79
+ )
80
+ diff_patch_evidence_id: Mapped[str | None] = mapped_column(
81
+ String(36),
82
+ ForeignKey("evidence.id", ondelete="SET NULL"),
83
+ nullable=True,
84
+ )
85
+ created_at: Mapped[datetime] = mapped_column(
86
+ DateTime,
87
+ server_default=func.now(),
88
+ nullable=False,
89
+ )
90
+
91
+ # Relationships
92
+ ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="revisions")
93
+ job: Mapped["Job"] = relationship(
94
+ "Job",
95
+ back_populates="revision",
96
+ foreign_keys=[job_id],
97
+ )
98
+ diff_stat_evidence: Mapped["Evidence | None"] = relationship(
99
+ "Evidence",
100
+ foreign_keys=[diff_stat_evidence_id],
101
+ )
102
+ diff_patch_evidence: Mapped["Evidence | None"] = relationship(
103
+ "Evidence",
104
+ foreign_keys=[diff_patch_evidence_id],
105
+ )
106
+ comments: Mapped[list["ReviewComment"]] = relationship(
107
+ "ReviewComment",
108
+ back_populates="revision",
109
+ cascade="all, delete-orphan",
110
+ order_by="ReviewComment.created_at",
111
+ )
112
+ review_summary: Mapped["ReviewSummary | None"] = relationship(
113
+ "ReviewSummary",
114
+ back_populates="revision",
115
+ cascade="all, delete-orphan",
116
+ uselist=False,
117
+ )
118
+
119
+ @property
120
+ def status_enum(self) -> RevisionStatus:
121
+ """Get the status as a RevisionStatus enum."""
122
+ return RevisionStatus(self.status)
123
+
124
+ @property
125
+ def unresolved_comment_count(self) -> int:
126
+ """Get count of unresolved comments."""
127
+ return sum(1 for c in self.comments if not c.resolved)
128
+
129
+ def __repr__(self) -> str:
130
+ return f"<Revision(id={self.id}, ticket_id={self.ticket_id}, number={self.number}, status={self.status})>"