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,102 @@
|
|
|
1
|
+
"""Add boards table and board_id to goals/tickets/jobs/workspaces.
|
|
2
|
+
|
|
3
|
+
Revision ID: 012
|
|
4
|
+
Revises: 011
|
|
5
|
+
Create Date: 2026-01-08
|
|
6
|
+
|
|
7
|
+
The Board is the primary permission boundary in Draft:
|
|
8
|
+
- All goals, tickets, jobs, workspaces belong to a board
|
|
9
|
+
- board_id is the authorization check for all operations
|
|
10
|
+
- repo_root is a property of the board (single repo per board)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
from alembic import op
|
|
16
|
+
|
|
17
|
+
# revision identifiers, used by Alembic.
|
|
18
|
+
revision: str = "012"
|
|
19
|
+
down_revision: str | None = "011"
|
|
20
|
+
branch_labels: str | tuple[str, ...] | None = None
|
|
21
|
+
depends_on: str | tuple[str, ...] | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def upgrade() -> None:
|
|
25
|
+
# Create boards table
|
|
26
|
+
op.create_table(
|
|
27
|
+
"boards",
|
|
28
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
29
|
+
sa.Column("name", sa.String(255), nullable=False),
|
|
30
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
31
|
+
sa.Column("repo_root", sa.String(1024), nullable=False),
|
|
32
|
+
sa.Column("default_branch", sa.String(255), nullable=True),
|
|
33
|
+
sa.Column(
|
|
34
|
+
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now()
|
|
35
|
+
),
|
|
36
|
+
sa.Column(
|
|
37
|
+
"updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Add board_id to goals (using batch mode for SQLite compatibility)
|
|
42
|
+
with op.batch_alter_table("goals") as batch_op:
|
|
43
|
+
batch_op.add_column(sa.Column("board_id", sa.String(36), nullable=True))
|
|
44
|
+
batch_op.create_index("ix_goals_board_id", ["board_id"])
|
|
45
|
+
batch_op.create_foreign_key(
|
|
46
|
+
"fk_goals_board_id", "boards", ["board_id"], ["id"], ondelete="CASCADE"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Add board_id to tickets
|
|
50
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
51
|
+
batch_op.add_column(sa.Column("board_id", sa.String(36), nullable=True))
|
|
52
|
+
batch_op.create_index("ix_tickets_board_id", ["board_id"])
|
|
53
|
+
batch_op.create_foreign_key(
|
|
54
|
+
"fk_tickets_board_id", "boards", ["board_id"], ["id"], ondelete="CASCADE"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Add board_id to jobs
|
|
58
|
+
with op.batch_alter_table("jobs") as batch_op:
|
|
59
|
+
batch_op.add_column(sa.Column("board_id", sa.String(36), nullable=True))
|
|
60
|
+
batch_op.create_index("ix_jobs_board_id", ["board_id"])
|
|
61
|
+
batch_op.create_foreign_key(
|
|
62
|
+
"fk_jobs_board_id", "boards", ["board_id"], ["id"], ondelete="CASCADE"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Add board_id to workspaces
|
|
66
|
+
with op.batch_alter_table("workspaces") as batch_op:
|
|
67
|
+
batch_op.add_column(sa.Column("board_id", sa.String(36), nullable=True))
|
|
68
|
+
batch_op.create_index("ix_workspaces_board_id", ["board_id"])
|
|
69
|
+
batch_op.create_foreign_key(
|
|
70
|
+
"fk_workspaces_board_id", "boards", ["board_id"], ["id"], ondelete="CASCADE"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def downgrade() -> None:
|
|
75
|
+
# Drop foreign keys and columns in reverse order (using batch mode for SQLite)
|
|
76
|
+
|
|
77
|
+
# workspaces
|
|
78
|
+
with op.batch_alter_table("workspaces") as batch_op:
|
|
79
|
+
batch_op.drop_constraint("fk_workspaces_board_id", type_="foreignkey")
|
|
80
|
+
batch_op.drop_index("ix_workspaces_board_id")
|
|
81
|
+
batch_op.drop_column("board_id")
|
|
82
|
+
|
|
83
|
+
# jobs
|
|
84
|
+
with op.batch_alter_table("jobs") as batch_op:
|
|
85
|
+
batch_op.drop_constraint("fk_jobs_board_id", type_="foreignkey")
|
|
86
|
+
batch_op.drop_index("ix_jobs_board_id")
|
|
87
|
+
batch_op.drop_column("board_id")
|
|
88
|
+
|
|
89
|
+
# tickets
|
|
90
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
91
|
+
batch_op.drop_constraint("fk_tickets_board_id", type_="foreignkey")
|
|
92
|
+
batch_op.drop_index("ix_tickets_board_id")
|
|
93
|
+
batch_op.drop_column("board_id")
|
|
94
|
+
|
|
95
|
+
# goals
|
|
96
|
+
with op.batch_alter_table("goals") as batch_op:
|
|
97
|
+
batch_op.drop_constraint("fk_goals_board_id", type_="foreignkey")
|
|
98
|
+
batch_op.drop_index("ix_goals_board_id")
|
|
99
|
+
batch_op.drop_column("board_id")
|
|
100
|
+
|
|
101
|
+
# boards table
|
|
102
|
+
op.drop_table("boards")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Add ticket blocking/dependency support.
|
|
2
|
+
|
|
3
|
+
Revision ID: 013
|
|
4
|
+
Revises: 012
|
|
5
|
+
Create Date: 2026-01-11
|
|
6
|
+
|
|
7
|
+
Adds blocked_by_ticket_id to tickets table to support ticket dependencies.
|
|
8
|
+
When a ticket is blocked by another ticket, it cannot be queued for execution
|
|
9
|
+
until the blocking ticket is completed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
from alembic import op
|
|
15
|
+
|
|
16
|
+
# revision identifiers, used by Alembic.
|
|
17
|
+
revision: str = "013"
|
|
18
|
+
down_revision: str | None = "012"
|
|
19
|
+
branch_labels: str | tuple[str, ...] | None = None
|
|
20
|
+
depends_on: str | tuple[str, ...] | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def upgrade() -> None:
|
|
24
|
+
# Add blocked_by_ticket_id to tickets (self-referential FK)
|
|
25
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
26
|
+
batch_op.add_column(
|
|
27
|
+
sa.Column("blocked_by_ticket_id", sa.String(36), nullable=True)
|
|
28
|
+
)
|
|
29
|
+
batch_op.create_index(
|
|
30
|
+
"ix_tickets_blocked_by_ticket_id", ["blocked_by_ticket_id"]
|
|
31
|
+
)
|
|
32
|
+
batch_op.create_foreign_key(
|
|
33
|
+
"fk_tickets_blocked_by_ticket_id",
|
|
34
|
+
"tickets",
|
|
35
|
+
["blocked_by_ticket_id"],
|
|
36
|
+
["id"],
|
|
37
|
+
ondelete="SET NULL", # If blocker is deleted, unblock this ticket
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def downgrade() -> None:
|
|
42
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
43
|
+
batch_op.drop_constraint("fk_tickets_blocked_by_ticket_id", type_="foreignkey")
|
|
44
|
+
batch_op.drop_index("ix_tickets_blocked_by_ticket_id")
|
|
45
|
+
batch_op.drop_column("blocked_by_ticket_id")
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Add agent sessions and messages tables for conversation continuity and cost tracking.
|
|
2
|
+
|
|
3
|
+
Revision ID: 014
|
|
4
|
+
Revises: 013
|
|
5
|
+
Create Date: 2026-01-12
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
from alembic import op
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "014"
|
|
17
|
+
down_revision: str | None = "013"
|
|
18
|
+
branch_labels: str | Sequence[str] | None = None
|
|
19
|
+
depends_on: str | Sequence[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def table_exists(table_name: str) -> bool:
|
|
23
|
+
"""Check if a table exists in SQLite."""
|
|
24
|
+
from alembic import op
|
|
25
|
+
|
|
26
|
+
conn = op.get_bind()
|
|
27
|
+
result = conn.execute(
|
|
28
|
+
sa.text(
|
|
29
|
+
f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'"
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
return result.fetchone() is not None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def column_exists(table_name: str, column_name: str) -> bool:
|
|
36
|
+
"""Check if a column exists in a SQLite table."""
|
|
37
|
+
from alembic import op
|
|
38
|
+
|
|
39
|
+
conn = op.get_bind()
|
|
40
|
+
result = conn.execute(sa.text(f"PRAGMA table_info({table_name})"))
|
|
41
|
+
columns = [row[1] for row in result.fetchall()]
|
|
42
|
+
return column_name in columns
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def index_exists(index_name: str) -> bool:
|
|
46
|
+
"""Check if an index exists in SQLite."""
|
|
47
|
+
from alembic import op
|
|
48
|
+
|
|
49
|
+
conn = op.get_bind()
|
|
50
|
+
result = conn.execute(
|
|
51
|
+
sa.text(
|
|
52
|
+
f"SELECT name FROM sqlite_master WHERE type='index' AND name='{index_name}'"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return result.fetchone() is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def upgrade() -> None:
|
|
59
|
+
# Create agent_sessions table if it doesn't exist
|
|
60
|
+
if not table_exists("agent_sessions"):
|
|
61
|
+
op.create_table(
|
|
62
|
+
"agent_sessions",
|
|
63
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
64
|
+
sa.Column(
|
|
65
|
+
"ticket_id",
|
|
66
|
+
sa.String(36),
|
|
67
|
+
sa.ForeignKey("tickets.id", ondelete="CASCADE"),
|
|
68
|
+
nullable=False,
|
|
69
|
+
index=True,
|
|
70
|
+
),
|
|
71
|
+
sa.Column(
|
|
72
|
+
"job_id",
|
|
73
|
+
sa.String(36),
|
|
74
|
+
sa.ForeignKey("jobs.id", ondelete="SET NULL"),
|
|
75
|
+
nullable=True,
|
|
76
|
+
index=True,
|
|
77
|
+
),
|
|
78
|
+
# Agent identification
|
|
79
|
+
sa.Column("agent_type", sa.String(50), nullable=False),
|
|
80
|
+
sa.Column(
|
|
81
|
+
"agent_session_id", sa.String(255), nullable=True
|
|
82
|
+
), # External session ID
|
|
83
|
+
# Session state
|
|
84
|
+
sa.Column("is_active", sa.Boolean, default=True, nullable=False),
|
|
85
|
+
sa.Column("turn_count", sa.Integer, default=0, nullable=False),
|
|
86
|
+
# Token tracking
|
|
87
|
+
sa.Column("total_input_tokens", sa.Integer, default=0, nullable=False),
|
|
88
|
+
sa.Column("total_output_tokens", sa.Integer, default=0, nullable=False),
|
|
89
|
+
sa.Column("estimated_cost_usd", sa.Float, default=0.0, nullable=False),
|
|
90
|
+
# Context
|
|
91
|
+
sa.Column("last_prompt", sa.Text, nullable=True),
|
|
92
|
+
sa.Column("last_response_summary", sa.Text, nullable=True),
|
|
93
|
+
sa.Column("metadata", sa.JSON, nullable=True),
|
|
94
|
+
# Timestamps
|
|
95
|
+
sa.Column(
|
|
96
|
+
"created_at", sa.DateTime, server_default=sa.func.now(), nullable=False
|
|
97
|
+
),
|
|
98
|
+
sa.Column(
|
|
99
|
+
"updated_at",
|
|
100
|
+
sa.DateTime,
|
|
101
|
+
server_default=sa.func.now(),
|
|
102
|
+
onupdate=sa.func.now(),
|
|
103
|
+
nullable=False,
|
|
104
|
+
),
|
|
105
|
+
sa.Column("ended_at", sa.DateTime, nullable=True),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Create agent_messages table if it doesn't exist
|
|
109
|
+
if not table_exists("agent_messages"):
|
|
110
|
+
op.create_table(
|
|
111
|
+
"agent_messages",
|
|
112
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
113
|
+
sa.Column(
|
|
114
|
+
"session_id",
|
|
115
|
+
sa.String(36),
|
|
116
|
+
sa.ForeignKey("agent_sessions.id", ondelete="CASCADE"),
|
|
117
|
+
nullable=False,
|
|
118
|
+
index=True,
|
|
119
|
+
),
|
|
120
|
+
# Message content
|
|
121
|
+
sa.Column(
|
|
122
|
+
"role", sa.String(20), nullable=False
|
|
123
|
+
), # user, assistant, system, tool
|
|
124
|
+
sa.Column("content", sa.Text, nullable=False),
|
|
125
|
+
# Token counts
|
|
126
|
+
sa.Column("input_tokens", sa.Integer, default=0, nullable=False),
|
|
127
|
+
sa.Column("output_tokens", sa.Integer, default=0, nullable=False),
|
|
128
|
+
# Tool tracking
|
|
129
|
+
sa.Column("tool_name", sa.String(100), nullable=True),
|
|
130
|
+
sa.Column("tool_input", sa.JSON, nullable=True),
|
|
131
|
+
sa.Column("tool_output", sa.Text, nullable=True),
|
|
132
|
+
# Metadata
|
|
133
|
+
sa.Column("metadata", sa.JSON, nullable=True),
|
|
134
|
+
sa.Column(
|
|
135
|
+
"created_at", sa.DateTime, server_default=sa.func.now(), nullable=False
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Create cost_budgets table if it doesn't exist
|
|
140
|
+
if not table_exists("cost_budgets"):
|
|
141
|
+
op.create_table(
|
|
142
|
+
"cost_budgets",
|
|
143
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
144
|
+
sa.Column(
|
|
145
|
+
"goal_id",
|
|
146
|
+
sa.String(36),
|
|
147
|
+
sa.ForeignKey("goals.id", ondelete="CASCADE"),
|
|
148
|
+
nullable=True,
|
|
149
|
+
index=True,
|
|
150
|
+
),
|
|
151
|
+
# Budget limits
|
|
152
|
+
sa.Column("daily_budget", sa.Float, nullable=True),
|
|
153
|
+
sa.Column("weekly_budget", sa.Float, nullable=True),
|
|
154
|
+
sa.Column("monthly_budget", sa.Float, nullable=True),
|
|
155
|
+
sa.Column("total_budget", sa.Float, nullable=True),
|
|
156
|
+
# Alerts
|
|
157
|
+
sa.Column(
|
|
158
|
+
"warning_threshold", sa.Float, default=0.8, nullable=False
|
|
159
|
+
), # 80%
|
|
160
|
+
sa.Column("pause_on_exceed", sa.Boolean, default=False, nullable=False),
|
|
161
|
+
# Timestamps
|
|
162
|
+
sa.Column(
|
|
163
|
+
"created_at", sa.DateTime, server_default=sa.func.now(), nullable=False
|
|
164
|
+
),
|
|
165
|
+
sa.Column(
|
|
166
|
+
"updated_at",
|
|
167
|
+
sa.DateTime,
|
|
168
|
+
server_default=sa.func.now(),
|
|
169
|
+
onupdate=sa.func.now(),
|
|
170
|
+
nullable=False,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Add agent_type column to jobs table if it doesn't exist
|
|
175
|
+
if not column_exists("jobs", "agent_type"):
|
|
176
|
+
op.add_column("jobs", sa.Column("agent_type", sa.String(50), nullable=True))
|
|
177
|
+
if not column_exists("jobs", "agent_session_id"):
|
|
178
|
+
op.add_column(
|
|
179
|
+
"jobs", sa.Column("agent_session_id", sa.String(36), nullable=True)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Add cost tracking to tickets if it doesn't exist
|
|
183
|
+
if not column_exists("tickets", "total_cost_usd"):
|
|
184
|
+
op.add_column(
|
|
185
|
+
"tickets", sa.Column("total_cost_usd", sa.Float, default=0.0, nullable=True)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Create indexes for performance if they don't exist
|
|
189
|
+
if not index_exists("ix_agent_sessions_created_at"):
|
|
190
|
+
op.create_index(
|
|
191
|
+
"ix_agent_sessions_created_at", "agent_sessions", ["created_at"]
|
|
192
|
+
)
|
|
193
|
+
if not index_exists("ix_agent_sessions_agent_type"):
|
|
194
|
+
op.create_index(
|
|
195
|
+
"ix_agent_sessions_agent_type", "agent_sessions", ["agent_type"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def downgrade() -> None:
|
|
200
|
+
# Drop indexes if they exist
|
|
201
|
+
if index_exists("ix_agent_sessions_agent_type"):
|
|
202
|
+
op.drop_index("ix_agent_sessions_agent_type", "agent_sessions")
|
|
203
|
+
if index_exists("ix_agent_sessions_created_at"):
|
|
204
|
+
op.drop_index("ix_agent_sessions_created_at", "agent_sessions")
|
|
205
|
+
|
|
206
|
+
# Drop columns from existing tables if they exist
|
|
207
|
+
if column_exists("tickets", "total_cost_usd"):
|
|
208
|
+
op.drop_column("tickets", "total_cost_usd")
|
|
209
|
+
if column_exists("jobs", "agent_session_id"):
|
|
210
|
+
op.drop_column("jobs", "agent_session_id")
|
|
211
|
+
if column_exists("jobs", "agent_type"):
|
|
212
|
+
op.drop_column("jobs", "agent_type")
|
|
213
|
+
|
|
214
|
+
# Drop new tables if they exist
|
|
215
|
+
if table_exists("cost_budgets"):
|
|
216
|
+
op.drop_table("cost_budgets")
|
|
217
|
+
if table_exists("agent_messages"):
|
|
218
|
+
op.drop_table("agent_messages")
|
|
219
|
+
if table_exists("agent_sessions"):
|
|
220
|
+
op.drop_table("agent_sessions")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Add sort_order column to tickets for drag-and-drop ordering.
|
|
2
|
+
|
|
3
|
+
Revision ID: 015
|
|
4
|
+
Revises: 774dc335c679
|
|
5
|
+
Create Date: 2026-03-01
|
|
6
|
+
|
|
7
|
+
Adds a sort_order integer column to the tickets table so that users
|
|
8
|
+
can manually reorder tickets within a state column via drag-and-drop.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
from alembic import op
|
|
16
|
+
|
|
17
|
+
# revision identifiers, used by Alembic.
|
|
18
|
+
revision: str = "015"
|
|
19
|
+
down_revision: str | None = "774dc335c679"
|
|
20
|
+
branch_labels: str | Sequence[str] | None = None
|
|
21
|
+
depends_on: str | Sequence[str] | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def upgrade() -> None:
|
|
25
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
26
|
+
batch_op.add_column(sa.Column("sort_order", sa.Integer(), nullable=True))
|
|
27
|
+
batch_op.create_index("ix_tickets_sort_order", ["sort_order"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def downgrade() -> None:
|
|
31
|
+
with op.batch_alter_table("tickets") as batch_op:
|
|
32
|
+
batch_op.drop_index("ix_tickets_sort_order")
|
|
33
|
+
batch_op.drop_column("sort_order")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""add_pr_fields_to_ticket
|
|
2
|
+
|
|
3
|
+
Revision ID: 03220f0b93ae
|
|
4
|
+
Revises: 8ef5054dc280
|
|
5
|
+
Create Date: 2026-01-12 11:20:58.959405
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
from alembic import op
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "03220f0b93ae"
|
|
17
|
+
down_revision: str | None = "8ef5054dc280"
|
|
18
|
+
branch_labels: str | Sequence[str] | None = None
|
|
19
|
+
depends_on: str | Sequence[str] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# Add PR fields to tickets table
|
|
24
|
+
op.add_column("tickets", sa.Column("pr_number", sa.Integer(), nullable=True))
|
|
25
|
+
op.add_column("tickets", sa.Column("pr_url", sa.String(length=500), nullable=True))
|
|
26
|
+
op.add_column("tickets", sa.Column("pr_state", sa.String(length=20), nullable=True))
|
|
27
|
+
op.add_column("tickets", sa.Column("pr_created_at", sa.DateTime(), nullable=True))
|
|
28
|
+
op.add_column("tickets", sa.Column("pr_merged_at", sa.DateTime(), nullable=True))
|
|
29
|
+
op.add_column(
|
|
30
|
+
"tickets", sa.Column("pr_head_branch", sa.String(length=255), nullable=True)
|
|
31
|
+
)
|
|
32
|
+
op.add_column(
|
|
33
|
+
"tickets", sa.Column("pr_base_branch", sa.String(length=255), nullable=True)
|
|
34
|
+
)
|
|
35
|
+
op.create_index(
|
|
36
|
+
op.f("ix_tickets_pr_number"), "tickets", ["pr_number"], unique=False
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def downgrade() -> None:
|
|
41
|
+
# Remove PR fields from tickets table
|
|
42
|
+
op.drop_index(op.f("ix_tickets_pr_number"), table_name="tickets")
|
|
43
|
+
op.drop_column("tickets", "pr_base_branch")
|
|
44
|
+
op.drop_column("tickets", "pr_head_branch")
|
|
45
|
+
op.drop_column("tickets", "pr_merged_at")
|
|
46
|
+
op.drop_column("tickets", "pr_created_at")
|
|
47
|
+
op.drop_column("tickets", "pr_state")
|
|
48
|
+
op.drop_column("tickets", "pr_url")
|
|
49
|
+
op.drop_column("tickets", "pr_number")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""seed_board_configs_from_yaml
|
|
2
|
+
|
|
3
|
+
Revision ID: 0c2d89fff3b1
|
|
4
|
+
Revises: 82ecd978cc70
|
|
5
|
+
Create Date: 2026-03-01 12:00:00.000000
|
|
6
|
+
|
|
7
|
+
Seed migration: populates Board.config for all existing boards.
|
|
8
|
+
|
|
9
|
+
For each board:
|
|
10
|
+
1. Load draft.yaml from board.repo_root (if it exists)
|
|
11
|
+
2. Deep-merge: DraftConfig defaults ← YAML values ← existing board.config overrides
|
|
12
|
+
3. Write the full merged config back to Board.config
|
|
13
|
+
|
|
14
|
+
This ensures every board has a complete, self-contained config in the DB
|
|
15
|
+
so the system no longer needs to read from YAML at runtime.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
from sqlalchemy import text
|
|
24
|
+
|
|
25
|
+
from alembic import op
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# revision identifiers, used by Alembic.
|
|
30
|
+
revision: str = "0c2d89fff3b1"
|
|
31
|
+
down_revision: str | None = "82ecd978cc70"
|
|
32
|
+
branch_labels: str | None = None
|
|
33
|
+
depends_on: str | None = None
|
|
34
|
+
|
|
35
|
+
CONFIG_FILENAME = "draft.yaml"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def deep_merge_dicts(base: dict, override: dict) -> dict:
|
|
39
|
+
"""Deep merge two dicts. override wins on conflicts."""
|
|
40
|
+
result = base.copy()
|
|
41
|
+
for key, value in override.items():
|
|
42
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
43
|
+
result[key] = deep_merge_dicts(result[key], value)
|
|
44
|
+
else:
|
|
45
|
+
result[key] = value
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_yaml_config(repo_root: str) -> dict | None:
|
|
50
|
+
"""Try to load draft.yaml from a repo root. Returns None on failure."""
|
|
51
|
+
config_path = Path(repo_root) / CONFIG_FILENAME
|
|
52
|
+
if not config_path.exists():
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
with open(config_path) as f:
|
|
56
|
+
data = yaml.safe_load(f)
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
return data
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.warning("Failed to load %s: %s", config_path, e)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_defaults() -> dict:
|
|
65
|
+
"""Build the full default config dict.
|
|
66
|
+
|
|
67
|
+
We inline the defaults here to make the migration self-contained and
|
|
68
|
+
deterministic — importing from app code could break if the dataclass
|
|
69
|
+
structure changes in a future version.
|
|
70
|
+
"""
|
|
71
|
+
return {
|
|
72
|
+
"project": {
|
|
73
|
+
"name": "Draft Project",
|
|
74
|
+
"repo_root": ".",
|
|
75
|
+
},
|
|
76
|
+
"execute_config": {
|
|
77
|
+
"executor": "claude",
|
|
78
|
+
"executor_model": "sonnet",
|
|
79
|
+
"timeout": 600,
|
|
80
|
+
"max_retries": 2,
|
|
81
|
+
"use_yolo": False,
|
|
82
|
+
"yolo_allowlist": [],
|
|
83
|
+
},
|
|
84
|
+
"verify_config": {
|
|
85
|
+
"commands": [],
|
|
86
|
+
"timeout": 300,
|
|
87
|
+
"stop_on_first_failure": True,
|
|
88
|
+
},
|
|
89
|
+
"planner_config": {
|
|
90
|
+
"enabled": False,
|
|
91
|
+
"model": "anthropic/claude-sonnet-4-20250514",
|
|
92
|
+
"interval_seconds": 2,
|
|
93
|
+
"max_followups_per_ticket": 2,
|
|
94
|
+
"max_followups_per_tick": 3,
|
|
95
|
+
"reflection_enabled": True,
|
|
96
|
+
"auto_verify": True,
|
|
97
|
+
"max_parallel_jobs": 1,
|
|
98
|
+
"features": {
|
|
99
|
+
"auto_execute": False,
|
|
100
|
+
"auto_followup": True,
|
|
101
|
+
"auto_reflection": True,
|
|
102
|
+
"validate_tickets": False,
|
|
103
|
+
},
|
|
104
|
+
"udar": {
|
|
105
|
+
"enabled": False,
|
|
106
|
+
"max_self_corrections": 3,
|
|
107
|
+
"significance_threshold": 0.2,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
"cleanup_config": {
|
|
111
|
+
"worktree_ttl_days": 7,
|
|
112
|
+
"evidence_ttl_days": 30,
|
|
113
|
+
"auto_cleanup_terminal": True,
|
|
114
|
+
},
|
|
115
|
+
"merge_config": {
|
|
116
|
+
"strategy": "merge",
|
|
117
|
+
"delete_branch_after_merge": True,
|
|
118
|
+
"pull_before_merge": True,
|
|
119
|
+
"require_pull_success": False,
|
|
120
|
+
},
|
|
121
|
+
"autonomy_config": {
|
|
122
|
+
"enabled": False,
|
|
123
|
+
"max_diff_lines": 500,
|
|
124
|
+
"sensitive_file_patterns": [
|
|
125
|
+
"*.env*",
|
|
126
|
+
"*secret*",
|
|
127
|
+
"*credential*",
|
|
128
|
+
"*password*",
|
|
129
|
+
"*.pem",
|
|
130
|
+
"*.key",
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
"executor_profiles": {},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def upgrade() -> None:
|
|
138
|
+
"""Seed Board.config for all existing boards."""
|
|
139
|
+
conn = op.get_bind()
|
|
140
|
+
|
|
141
|
+
rows = conn.execute(text("SELECT id, repo_root, config FROM boards")).fetchall()
|
|
142
|
+
|
|
143
|
+
if not rows:
|
|
144
|
+
logger.info("No boards found — nothing to seed.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
defaults = _get_defaults()
|
|
148
|
+
updated = 0
|
|
149
|
+
|
|
150
|
+
for row in rows:
|
|
151
|
+
board_id = row[0]
|
|
152
|
+
repo_root = row[1]
|
|
153
|
+
existing_config = row[2]
|
|
154
|
+
|
|
155
|
+
# Parse existing config (may be JSON string or dict depending on driver)
|
|
156
|
+
if existing_config is None:
|
|
157
|
+
existing = {}
|
|
158
|
+
elif isinstance(existing_config, str):
|
|
159
|
+
try:
|
|
160
|
+
existing = json.loads(existing_config)
|
|
161
|
+
except (json.JSONDecodeError, TypeError):
|
|
162
|
+
existing = {}
|
|
163
|
+
elif isinstance(existing_config, dict):
|
|
164
|
+
existing = existing_config
|
|
165
|
+
else:
|
|
166
|
+
existing = {}
|
|
167
|
+
|
|
168
|
+
# Try loading YAML from repo_root
|
|
169
|
+
yaml_config = _load_yaml_config(repo_root) if repo_root else None
|
|
170
|
+
|
|
171
|
+
# Deep-merge: defaults ← YAML ← existing board overrides
|
|
172
|
+
merged = defaults.copy()
|
|
173
|
+
if yaml_config:
|
|
174
|
+
merged = deep_merge_dicts(merged, yaml_config)
|
|
175
|
+
if existing:
|
|
176
|
+
merged = deep_merge_dicts(merged, existing)
|
|
177
|
+
|
|
178
|
+
# Write back
|
|
179
|
+
conn.execute(
|
|
180
|
+
text("UPDATE boards SET config = :config WHERE id = :id"),
|
|
181
|
+
{"config": json.dumps(merged), "id": board_id},
|
|
182
|
+
)
|
|
183
|
+
updated += 1
|
|
184
|
+
|
|
185
|
+
source = "defaults"
|
|
186
|
+
if yaml_config and existing:
|
|
187
|
+
source = "defaults + YAML + existing overrides"
|
|
188
|
+
elif yaml_config:
|
|
189
|
+
source = "defaults + YAML"
|
|
190
|
+
elif existing:
|
|
191
|
+
source = "defaults + existing overrides"
|
|
192
|
+
logger.info("Board %s: seeded config from %s", board_id, source)
|
|
193
|
+
|
|
194
|
+
logger.info("Seeded config for %d board(s).", updated)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def downgrade() -> None:
|
|
198
|
+
"""Revert boards to their pre-seed config state.
|
|
199
|
+
|
|
200
|
+
We cannot perfectly restore the original state (we don't know what
|
|
201
|
+
it was), so we set config to NULL which means 'use defaults'.
|
|
202
|
+
This is safe because the old code path falls back to defaults anyway.
|
|
203
|
+
"""
|
|
204
|
+
conn = op.get_bind()
|
|
205
|
+
conn.execute(text("UPDATE boards SET config = NULL"))
|
|
206
|
+
logger.info("Cleared Board.config for all boards (reverted to NULL/defaults).")
|