code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
coord/db.py
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
"""SQLite connection management and schema for coordinator state.
|
|
2
|
+
|
|
3
|
+
Single DB at ~/.coord/coord.db with WAL mode. All coordinator state lives
|
|
4
|
+
here: assignments, proposals, merge queue, sessions, etc.
|
|
5
|
+
|
|
6
|
+
Usage
|
|
7
|
+
-----
|
|
8
|
+
- Production code: ``get_connection()`` returns the singleton connection.
|
|
9
|
+
- Tests: call ``override_connection(sqlite3.connect(":memory:"))`` then
|
|
10
|
+
``close()`` in teardown (the ``coord_db`` fixture in conftest.py does this).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from coord.platform_paths import default_coord_dir
|
|
21
|
+
|
|
22
|
+
COORD_DIR = default_coord_dir()
|
|
23
|
+
DB_PATH = COORD_DIR / "coord.db"
|
|
24
|
+
|
|
25
|
+
_conn: sqlite3.Connection | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProductionDatabaseGuardError(RuntimeError):
|
|
29
|
+
"""Raised when code running under pytest tries to open the live
|
|
30
|
+
``~/.coord/coord.db`` (#1960).
|
|
31
|
+
|
|
32
|
+
The autouse ``coord_db`` fixture in ``tests/conftest.py`` overrides the
|
|
33
|
+
module-level singleton with an isolated ``:memory:`` connection before
|
|
34
|
+
every test body runs, so ``get_connection()`` normally never reaches
|
|
35
|
+
``_open()`` at all during a test. This guard exists for the paths that
|
|
36
|
+
fixture can't reach: code that calls ``_open(DB_PATH)`` directly, a
|
|
37
|
+
subprocess the test spawned that re-imports ``coord.db`` fresh (pytest's
|
|
38
|
+
``PYTEST_CURRENT_TEST`` env var is inherited by child processes by
|
|
39
|
+
default), or a test that closes the override and lets the singleton fall
|
|
40
|
+
back to the real path.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_connection() -> sqlite3.Connection:
|
|
45
|
+
"""Return the module-level singleton connection, opening it on first call."""
|
|
46
|
+
global _conn
|
|
47
|
+
if _conn is None:
|
|
48
|
+
_conn = _open(DB_PATH)
|
|
49
|
+
return _conn
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _open(path: Path) -> sqlite3.Connection:
|
|
53
|
+
if path == DB_PATH and os.environ.get("PYTEST_CURRENT_TEST"):
|
|
54
|
+
raise ProductionDatabaseGuardError(
|
|
55
|
+
f"Refusing to open the production coordinator database at "
|
|
56
|
+
f"{path} while running under pytest "
|
|
57
|
+
f"(PYTEST_CURRENT_TEST={os.environ['PYTEST_CURRENT_TEST']!r}). "
|
|
58
|
+
"A test (or a subprocess it spawned) resolved the real "
|
|
59
|
+
"~/.coord/coord.db instead of an isolated one -- see #1960. "
|
|
60
|
+
"Fix: rely on the autouse `coord_db` fixture (already active for "
|
|
61
|
+
"every test and overrides this singleton with an in-memory DB), "
|
|
62
|
+
"or pass an explicit isolated path instead of coord.db.DB_PATH."
|
|
63
|
+
)
|
|
64
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
conn = sqlite3.connect(str(path), check_same_thread=False)
|
|
66
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
67
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
68
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
69
|
+
conn.row_factory = sqlite3.Row
|
|
70
|
+
_ensure_schema(conn)
|
|
71
|
+
_maybe_migrate_json(conn)
|
|
72
|
+
_migrate_gate_order(conn)
|
|
73
|
+
_backfill_orphaned_review_verdicts(conn)
|
|
74
|
+
return conn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def override_connection(conn: sqlite3.Connection) -> None:
|
|
78
|
+
"""Replace the singleton connection. Used in tests to inject :memory: DBs."""
|
|
79
|
+
global _conn
|
|
80
|
+
_conn = conn
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def close() -> None:
|
|
84
|
+
"""Close the singleton connection and reset it to None."""
|
|
85
|
+
global _conn
|
|
86
|
+
if _conn is not None:
|
|
87
|
+
_conn.close()
|
|
88
|
+
_conn = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── Schema ────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def _ensure_schema(conn: sqlite3.Connection) -> None:
|
|
94
|
+
"""Create all tables and indexes if they don't already exist."""
|
|
95
|
+
conn.executescript("""
|
|
96
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
97
|
+
version INTEGER NOT NULL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS assignments (
|
|
101
|
+
assignment_id TEXT PRIMARY KEY,
|
|
102
|
+
machine_name TEXT NOT NULL,
|
|
103
|
+
repo_name TEXT NOT NULL,
|
|
104
|
+
repo_github TEXT,
|
|
105
|
+
issue_number INTEGER NOT NULL,
|
|
106
|
+
issue_title TEXT NOT NULL,
|
|
107
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
108
|
+
type TEXT NOT NULL DEFAULT 'work',
|
|
109
|
+
branch TEXT,
|
|
110
|
+
pr_url TEXT,
|
|
111
|
+
briefing TEXT DEFAULT '',
|
|
112
|
+
files_allowed TEXT DEFAULT '[]',
|
|
113
|
+
files_forbidden TEXT DEFAULT '[]',
|
|
114
|
+
model TEXT,
|
|
115
|
+
dispatched_at REAL,
|
|
116
|
+
finished_at REAL,
|
|
117
|
+
smoke_test TEXT,
|
|
118
|
+
smoke_test_reason TEXT,
|
|
119
|
+
review_state TEXT,
|
|
120
|
+
review_of_assignment_id TEXT,
|
|
121
|
+
review_target TEXT,
|
|
122
|
+
required_gates TEXT DEFAULT '[]',
|
|
123
|
+
plan TEXT,
|
|
124
|
+
unreachable_count INTEGER DEFAULT 0,
|
|
125
|
+
exit_code INTEGER,
|
|
126
|
+
review_iteration INTEGER DEFAULT 0,
|
|
127
|
+
review_posted_at REAL,
|
|
128
|
+
test_state TEXT,
|
|
129
|
+
test_reason TEXT,
|
|
130
|
+
cost_usd REAL,
|
|
131
|
+
smoke_tests TEXT,
|
|
132
|
+
review_findings TEXT,
|
|
133
|
+
test_plan TEXT
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
137
|
+
assignment_id TEXT PRIMARY KEY,
|
|
138
|
+
event TEXT NOT NULL,
|
|
139
|
+
branch TEXT,
|
|
140
|
+
posted_at REAL NOT NULL
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS proposals (
|
|
144
|
+
id INTEGER PRIMARY KEY,
|
|
145
|
+
machine_name TEXT NOT NULL,
|
|
146
|
+
repo_name TEXT NOT NULL,
|
|
147
|
+
issue_number INTEGER NOT NULL,
|
|
148
|
+
issue_title TEXT NOT NULL,
|
|
149
|
+
rationale TEXT DEFAULT '',
|
|
150
|
+
files_likely TEXT DEFAULT '[]',
|
|
151
|
+
briefing TEXT DEFAULT '',
|
|
152
|
+
model TEXT,
|
|
153
|
+
type TEXT DEFAULT 'work',
|
|
154
|
+
required_gates TEXT DEFAULT '[]'
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE TABLE IF NOT EXISTS split_proposals (
|
|
158
|
+
id INTEGER PRIMARY KEY,
|
|
159
|
+
repo_name TEXT NOT NULL,
|
|
160
|
+
issue_number INTEGER NOT NULL,
|
|
161
|
+
issue_title TEXT NOT NULL,
|
|
162
|
+
rationale TEXT DEFAULT ''
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE TABLE IF NOT EXISTS split_chunks (
|
|
166
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
167
|
+
split_proposal_id INTEGER NOT NULL REFERENCES split_proposals(id),
|
|
168
|
+
title TEXT NOT NULL,
|
|
169
|
+
scope TEXT NOT NULL,
|
|
170
|
+
files_likely TEXT DEFAULT '[]'
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS merge_queue (
|
|
174
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
|
+
assignment_id TEXT NOT NULL,
|
|
176
|
+
repo_name TEXT NOT NULL,
|
|
177
|
+
repo_github TEXT NOT NULL,
|
|
178
|
+
branch TEXT NOT NULL,
|
|
179
|
+
target_branch TEXT NOT NULL,
|
|
180
|
+
issue_number INTEGER NOT NULL,
|
|
181
|
+
issue_title TEXT NOT NULL,
|
|
182
|
+
state TEXT NOT NULL DEFAULT 'pending',
|
|
183
|
+
pr_number INTEGER,
|
|
184
|
+
pr_url TEXT,
|
|
185
|
+
size INTEGER,
|
|
186
|
+
last_attempt REAL,
|
|
187
|
+
error TEXT,
|
|
188
|
+
enqueued_at REAL,
|
|
189
|
+
assignment_type TEXT DEFAULT 'work',
|
|
190
|
+
required_gates TEXT,
|
|
191
|
+
ci_infra_reruns INTEGER NOT NULL DEFAULT 0
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
195
|
+
assignment_id TEXT PRIMARY KEY,
|
|
196
|
+
plan_data TEXT NOT NULL
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
201
|
+
started_at TEXT,
|
|
202
|
+
ended_at TEXT,
|
|
203
|
+
clean_shutdown INTEGER DEFAULT 0,
|
|
204
|
+
completed_this_session TEXT,
|
|
205
|
+
issues_closed TEXT,
|
|
206
|
+
total_cost_usd REAL
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
210
|
+
name TEXT PRIMARY KEY,
|
|
211
|
+
host TEXT NOT NULL,
|
|
212
|
+
capabilities TEXT DEFAULT '[]',
|
|
213
|
+
repos TEXT DEFAULT '[]'
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
CREATE TABLE IF NOT EXISTS board_meta (
|
|
217
|
+
key TEXT PRIMARY KEY,
|
|
218
|
+
value TEXT
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS issues (
|
|
222
|
+
repo_name TEXT NOT NULL,
|
|
223
|
+
number INTEGER NOT NULL,
|
|
224
|
+
title TEXT NOT NULL DEFAULT '',
|
|
225
|
+
body TEXT NOT NULL DEFAULT '',
|
|
226
|
+
state TEXT NOT NULL DEFAULT 'open',
|
|
227
|
+
labels TEXT NOT NULL DEFAULT '[]',
|
|
228
|
+
synced_at REAL,
|
|
229
|
+
PRIMARY KEY (repo_name, number)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
-- #603: per-issue rolling context digest. Short-lived curated notes
|
|
233
|
+
-- (cross-repo deps, failed approaches, hard constraints) that every
|
|
234
|
+
-- agent reads at the top of its briefing. Structured entries rendered
|
|
235
|
+
-- to markdown at read time; pinned rows stay on top, newest-first
|
|
236
|
+
-- below, oldest non-pinned aged out by a budget; dropped on close.
|
|
237
|
+
CREATE TABLE IF NOT EXISTS issue_context (
|
|
238
|
+
id INTEGER PRIMARY KEY,
|
|
239
|
+
repo_name TEXT NOT NULL,
|
|
240
|
+
issue_number INTEGER NOT NULL,
|
|
241
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
242
|
+
source TEXT,
|
|
243
|
+
body TEXT NOT NULL,
|
|
244
|
+
created_at REAL NOT NULL
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
-- #1036: durable, append-only audit trail. One row per real
|
|
248
|
+
-- state-changing transition, captured at the state._*_local /
|
|
249
|
+
-- issue_store write choke points (never at the ~30 CLI call
|
|
250
|
+
-- sites) so the log is topology-agnostic (thin-client vs daemon).
|
|
251
|
+
-- Modeled on issue_context above: additive, no UPDATE/DELETE
|
|
252
|
+
-- except the opportunistic audit.max_rows trim in coord/audit.py.
|
|
253
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
254
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
255
|
+
ts REAL NOT NULL,
|
|
256
|
+
tier TEXT NOT NULL,
|
|
257
|
+
category TEXT NOT NULL,
|
|
258
|
+
event_type TEXT NOT NULL,
|
|
259
|
+
actor TEXT NOT NULL,
|
|
260
|
+
repo TEXT,
|
|
261
|
+
issue INTEGER,
|
|
262
|
+
assignment_id TEXT,
|
|
263
|
+
machine TEXT,
|
|
264
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
265
|
+
details_json TEXT
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
-- #873: durable, queryable mirror of GitHub issue comments — the
|
|
269
|
+
-- prose message bus (completion summaries, review bodies, failure
|
|
270
|
+
-- reports) today lives only on GitHub. Populated two ways: (1)
|
|
271
|
+
-- capture-at-write, instrumented at the single choke point
|
|
272
|
+
-- coord.github_ops.post_issue_comment (which close_issue/close_pr
|
|
273
|
+
-- also funnel through), so a coord-authored comment is durable the
|
|
274
|
+
-- instant it posts, regardless of which machine posted it; (2) the
|
|
275
|
+
-- backfill sync (coord.state.sync_issue_comments), which upserts
|
|
276
|
+
-- human + out-of-band comments idempotently keyed on gh_comment_id.
|
|
277
|
+
-- Retained INDEFINITELY — never pruned on close, unlike the
|
|
278
|
+
-- short-lived issue_context digest above.
|
|
279
|
+
--
|
|
280
|
+
-- repo_name here holds whatever identifier the write site had —
|
|
281
|
+
-- in practice the GitHub "owner/repo" slug (github_ops functions
|
|
282
|
+
-- only ever see the slug, never the coordinator's config repo
|
|
283
|
+
-- key) — which differs from the issues/issue_context tables above
|
|
284
|
+
-- where repo_name is the coordinator.yml repo key. Documented here
|
|
285
|
+
-- since it's a real inconsistency a future reader could trip on.
|
|
286
|
+
--
|
|
287
|
+
-- created_at/updated_at are epoch seconds (REAL), matching the
|
|
288
|
+
-- rest of this schema's timestamp columns. body_ref is reserved,
|
|
289
|
+
-- unused for now — the future Azure-blob offload seam.
|
|
290
|
+
CREATE TABLE IF NOT EXISTS issue_comments (
|
|
291
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
292
|
+
gh_comment_id INTEGER UNIQUE,
|
|
293
|
+
repo_name TEXT NOT NULL,
|
|
294
|
+
issue_number INTEGER NOT NULL,
|
|
295
|
+
author TEXT,
|
|
296
|
+
created_at REAL,
|
|
297
|
+
updated_at REAL,
|
|
298
|
+
body TEXT NOT NULL DEFAULT '',
|
|
299
|
+
body_ref TEXT,
|
|
300
|
+
coord_event TEXT,
|
|
301
|
+
coord_assignment_id TEXT,
|
|
302
|
+
machine TEXT,
|
|
303
|
+
verdict TEXT
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
-- #1505: board-visible "driver stuck" record. Written by `coord
|
|
307
|
+
-- drive`'s merge stage the moment it hits a status no amount of
|
|
308
|
+
-- retrying can fix (NEEDS_ATTENTION / an unrecognised status) instead
|
|
309
|
+
-- of silently burning the merge-attempt budget on it (see
|
|
310
|
+
-- coord/drive.py's `_decide_merge`). One row per (repo_name,
|
|
311
|
+
-- issue_number) — a fresh escalation replaces the previous one
|
|
312
|
+
-- (`ON CONFLICT ... DO UPDATE`, coord/state.py
|
|
313
|
+
-- `_record_drive_escalation_local`); `coord escalate dismiss` (or the
|
|
314
|
+
-- TUI's "Dismiss" menu item) deletes it outright. `gate_readings` is
|
|
315
|
+
-- a human-readable "key=value | key=value" summary, not JSON — the
|
|
316
|
+
-- whole record exists to be read by a human via the Pipeline row's
|
|
317
|
+
-- right-click menu, not machine-parsed.
|
|
318
|
+
CREATE TABLE IF NOT EXISTS drive_escalations (
|
|
319
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
320
|
+
repo_name TEXT NOT NULL,
|
|
321
|
+
issue_number INTEGER NOT NULL,
|
|
322
|
+
stage TEXT NOT NULL DEFAULT 'merge',
|
|
323
|
+
assignment_id TEXT,
|
|
324
|
+
reason TEXT NOT NULL,
|
|
325
|
+
gate_readings TEXT NOT NULL DEFAULT '',
|
|
326
|
+
proposed_command TEXT NOT NULL DEFAULT '',
|
|
327
|
+
created_at REAL NOT NULL,
|
|
328
|
+
UNIQUE(repo_name, issue_number)
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
-- #1753 (DQ-1): the operator-declared `coord drive` work queue. One
|
|
332
|
+
-- row per (repo_name, issue_number) — enqueueing an issue that is
|
|
333
|
+
-- already queued updates it in place (`ON CONFLICT ... DO UPDATE`,
|
|
334
|
+
-- coord/state.py `_enqueue_drive_queue_local`) rather than creating a
|
|
335
|
+
-- second row, so the queue can never hold the same issue twice.
|
|
336
|
+
-- Written by `coord drive-queue` (DQ-2) and by the tick processor that
|
|
337
|
+
-- launches entries; cleared by `coord drive-queue remove` (dequeue) —
|
|
338
|
+
-- terminal rows (`done`/`failed`) stay put until an operator removes
|
|
339
|
+
-- them, so the list doubles as a short run history.
|
|
340
|
+
--
|
|
341
|
+
-- `position` is DENSE and 0-BASED: enqueue-without-a-position appends
|
|
342
|
+
-- at max(position)+1 and `move` renumbers the affected span in one
|
|
343
|
+
-- transaction (no fractional positions — the queue is short by nature
|
|
344
|
+
-- and `coord drive-queue list` prints a dense integer order).
|
|
345
|
+
-- `machine` NULL means "let `coord drive` route it". `after_json` is
|
|
346
|
+
-- a JSON list of FULLY QUALIFIED pre-req keys (`["repo#N", ...]`) so a
|
|
347
|
+
-- cross-repo queue needs no second column; it is decoded to a real
|
|
348
|
+
-- list on the wire via coord/dao.py's `_JSON_COLUMNS`. This table
|
|
349
|
+
-- only STORES `after_json` — interpreting it (pre-req satisfaction) is
|
|
350
|
+
-- the tick processor's job, not the schema's.
|
|
351
|
+
--
|
|
352
|
+
-- #1757 (deploy gates) added the five `hold_*`/`resume_when` columns
|
|
353
|
+
-- below. They are declared here so a FRESH database gets them from
|
|
354
|
+
-- the CREATE, and repeated in `_migrate_add_columns` so an EXISTING
|
|
355
|
+
-- ~/.coord/coord.db (DQ-1 shipped before this) gains them in place —
|
|
356
|
+
-- SQLite has no "ADD COLUMN IF NOT EXISTS", hence both. See
|
|
357
|
+
-- coord/drive_queue.py for the lifecycle those columns encode.
|
|
358
|
+
--
|
|
359
|
+
-- #1870: `launch_host` is the SHORT HOSTNAME of the machine whose tick
|
|
360
|
+
-- actually ran `coord drive --tmux` for this entry — set alongside
|
|
361
|
+
-- `session_name`/`launched_at` when the launch succeeds, '' for a row
|
|
362
|
+
-- that predates this column or was hand-flipped to `running`.
|
|
363
|
+
-- Liveness (`coord.drive.list_drive_sessions`) is always a LOCAL tmux
|
|
364
|
+
-- read; without this column a tick running on a DIFFERENT host than
|
|
365
|
+
-- the one that launched the drive has no way to know that, sees no
|
|
366
|
+
-- session, and reaps a healthy drive out from under another machine
|
|
367
|
+
-- (the 2026-08-06 incident: a drive 47 minutes into Test on
|
|
368
|
+
-- `elitebook` was declared dead and relaunched by the timer on
|
|
369
|
+
-- `dellserver`). `_reconcile_running` treats a mismatch as UNKNOWN,
|
|
370
|
+
-- never as dead — see coord/drive_queue.py.
|
|
371
|
+
CREATE TABLE IF NOT EXISTS drive_queue (
|
|
372
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
373
|
+
repo_name TEXT NOT NULL,
|
|
374
|
+
issue_number INTEGER NOT NULL,
|
|
375
|
+
position INTEGER NOT NULL,
|
|
376
|
+
machine TEXT,
|
|
377
|
+
after_json TEXT NOT NULL DEFAULT '[]',
|
|
378
|
+
state TEXT NOT NULL DEFAULT 'waiting',
|
|
379
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
380
|
+
deferrals INTEGER NOT NULL DEFAULT 0,
|
|
381
|
+
last_reason TEXT NOT NULL DEFAULT '',
|
|
382
|
+
session_name TEXT,
|
|
383
|
+
launched_at REAL,
|
|
384
|
+
enqueued_at REAL NOT NULL,
|
|
385
|
+
hold_after INTEGER NOT NULL DEFAULT 0,
|
|
386
|
+
hold_reason TEXT NOT NULL DEFAULT '',
|
|
387
|
+
resume_when TEXT NOT NULL DEFAULT '',
|
|
388
|
+
hold_state TEXT NOT NULL DEFAULT '',
|
|
389
|
+
hold_probes INTEGER NOT NULL DEFAULT 0,
|
|
390
|
+
launch_host TEXT NOT NULL DEFAULT '',
|
|
391
|
+
UNIQUE(repo_name, issue_number)
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
-- #1630: daemon's aggregated view of each agent's /health payload
|
|
395
|
+
-- (H-1's check-registry results, when the agent reports them) plus
|
|
396
|
+
-- the poll's own reachability verdict. One row per machine, always
|
|
397
|
+
-- overwritten in place (`ON CONFLICT ... DO UPDATE`,
|
|
398
|
+
-- coord/state.py `save_machine_health`) — this is a live snapshot,
|
|
399
|
+
-- not a history. `received_at` is THIS daemon's clock at poll time,
|
|
400
|
+
-- not anything the agent claims, so staleness detection (an agent
|
|
401
|
+
-- that stops reporting must read `unknown`, never "still green")
|
|
402
|
+
-- can never be fooled by a stopped agent's stale self-reported
|
|
403
|
+
-- timestamp. `health_json` is NULL for an unreachable machine or an
|
|
404
|
+
-- agent too old to report a health block — advisory only, never
|
|
405
|
+
-- consulted by dispatch/routing/merge-queue code (see
|
|
406
|
+
-- tests/test_health_advisory_only.py).
|
|
407
|
+
CREATE TABLE IF NOT EXISTS machine_health (
|
|
408
|
+
machine_name TEXT PRIMARY KEY,
|
|
409
|
+
state TEXT NOT NULL,
|
|
410
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
411
|
+
latency_ms REAL,
|
|
412
|
+
health_json TEXT,
|
|
413
|
+
received_at REAL NOT NULL
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
-- #2048: per-assignment tracking state for the cheap per-turn
|
|
417
|
+
-- liveness auditor (coord/liveness_auditor.py). One row per
|
|
418
|
+
-- assignment ever audited; consecutive_blocked is the running
|
|
419
|
+
-- streak of BLOCKED verdicts (reset to 0 by any continue/done),
|
|
420
|
+
-- and `raised` flips to 1 the one time the streak first reaches
|
|
421
|
+
-- the configured strike count, so a stall is reported once rather
|
|
422
|
+
-- than on every subsequent poll while the worker stays blocked.
|
|
423
|
+
-- Read/written ONLY by coord.notify.detect_liveness_stall and
|
|
424
|
+
-- coord.state's accessors below — never by anything that also
|
|
425
|
+
-- touches Assignment.status/review_state/test_state (the auditor
|
|
426
|
+
-- is a tripwire, not a gate).
|
|
427
|
+
CREATE TABLE IF NOT EXISTS liveness_audits (
|
|
428
|
+
assignment_id TEXT PRIMARY KEY,
|
|
429
|
+
consecutive_blocked INTEGER NOT NULL DEFAULT 0,
|
|
430
|
+
last_audit_at REAL,
|
|
431
|
+
last_verdict TEXT,
|
|
432
|
+
raised INTEGER NOT NULL DEFAULT 0
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_assignments_status ON assignments(status);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_assignments_machine ON assignments(machine_name);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_merge_queue_state ON merge_queue(state);
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_issue_context_issue
|
|
439
|
+
ON issue_context(repo_name, issue_number);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_ts ON audit_log(ts);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_assignment ON audit_log(assignment_id);
|
|
442
|
+
CREATE INDEX IF NOT EXISTS idx_issue_comments_issue
|
|
443
|
+
ON issue_comments(repo_name, issue_number);
|
|
444
|
+
CREATE INDEX IF NOT EXISTS idx_drive_escalations_issue
|
|
445
|
+
ON drive_escalations(repo_name, issue_number);
|
|
446
|
+
CREATE INDEX IF NOT EXISTS idx_drive_queue_state
|
|
447
|
+
ON drive_queue(state, position);
|
|
448
|
+
|
|
449
|
+
INSERT OR IGNORE INTO schema_version VALUES (1);
|
|
450
|
+
""")
|
|
451
|
+
conn.commit()
|
|
452
|
+
# Column-level migrations for existing databases. SQLite does not support
|
|
453
|
+
# "ADD COLUMN IF NOT EXISTS", so we catch OperationalError instead.
|
|
454
|
+
_migrate_add_columns(conn)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _migrate_add_columns(conn: sqlite3.Connection) -> None:
|
|
458
|
+
"""Add new columns to existing databases via ALTER TABLE.
|
|
459
|
+
|
|
460
|
+
Safe to call on databases that already have the columns — the
|
|
461
|
+
OperationalError raised by SQLite is silently swallowed.
|
|
462
|
+
"""
|
|
463
|
+
migrations = [
|
|
464
|
+
"ALTER TABLE assignments ADD COLUMN review_iteration INTEGER DEFAULT 0",
|
|
465
|
+
"ALTER TABLE assignments ADD COLUMN review_posted_at REAL",
|
|
466
|
+
# #200: human-driven Test gate between Work and Review.
|
|
467
|
+
"ALTER TABLE assignments ADD COLUMN test_state TEXT",
|
|
468
|
+
"ALTER TABLE assignments ADD COLUMN test_reason TEXT",
|
|
469
|
+
# #253: persisted adversarial-review verdict so the merge gate can
|
|
470
|
+
# check approval without re-parsing logs after restart.
|
|
471
|
+
"ALTER TABLE assignments ADD COLUMN review_verdict TEXT",
|
|
472
|
+
# #208: worker cost captured from the final stream-json result event.
|
|
473
|
+
"ALTER TABLE assignments ADD COLUMN cost_usd REAL",
|
|
474
|
+
# #252: worker-emitted smoke-test list (JSON array of strings;
|
|
475
|
+
# NULL = not emitted, '[]' = explicit "(none — internal)").
|
|
476
|
+
"ALTER TABLE assignments ADD COLUMN smoke_tests TEXT",
|
|
477
|
+
# #bounce: cached review-findings body (markdown text) so coord
|
|
478
|
+
# bounce + the upcoming per-stage display don't have to re-fetch
|
|
479
|
+
# the review log from the agent. Populated by notify.py when
|
|
480
|
+
# the review is first parsed. NULL = not yet parsed; populated
|
|
481
|
+
# = full findings.body text.
|
|
482
|
+
"ALTER TABLE assignments ADD COLUMN review_findings TEXT",
|
|
483
|
+
# #315: claude session ID captured from the worker's `system.init`
|
|
484
|
+
# event. Set by notify.py after the agent reports completion. Used
|
|
485
|
+
# by `coord chat-continue` to pass `--resume <id>` to the next
|
|
486
|
+
# worker so it loads the prior conversation and continues it.
|
|
487
|
+
"ALTER TABLE assignments ADD COLUMN claude_session_id TEXT",
|
|
488
|
+
# #342 Phase A: AI-generated smoke test plan (JSON-encoded).
|
|
489
|
+
# NULL = not yet generated. Set by `coord test-plan` and read back
|
|
490
|
+
# by the CLI (cache hit) and eventually by the TUI (Phase B).
|
|
491
|
+
"ALTER TABLE assignments ADD COLUMN test_plan TEXT",
|
|
492
|
+
# #349 Phase B: branch HEAD SHA at the time `coord test-plan` last ran.
|
|
493
|
+
# Used by the TUI to detect staleness: if the branch has advanced
|
|
494
|
+
# since the plan was generated, it re-runs `coord test-plan --refresh`
|
|
495
|
+
# automatically. NULL = plan not yet generated, or generated without
|
|
496
|
+
# branch tracking (legacy). Always reset to NULL when set_test_plan
|
|
497
|
+
# is called with branch_head=None so no stale SHA persists.
|
|
498
|
+
"ALTER TABLE assignments ADD COLUMN test_plan_branch_head TEXT",
|
|
499
|
+
# #406 Phase A: milestone columns on the issues table.
|
|
500
|
+
# milestone_number is the GitHub milestone number (integer id); NULL for
|
|
501
|
+
# unassigned. milestone_title is the human-readable name (e.g. "v0.5");
|
|
502
|
+
# NULL when no milestone is assigned. Idempotent — SQLite raises
|
|
503
|
+
# OperationalError when the column already exists, which is swallowed
|
|
504
|
+
# below.
|
|
505
|
+
"ALTER TABLE issues ADD COLUMN milestone_number INTEGER",
|
|
506
|
+
"ALTER TABLE issues ADD COLUMN milestone_title TEXT",
|
|
507
|
+
# #324: resolved provider name recorded at dispatch time so the TUI
|
|
508
|
+
# can surface it in the assignment detail panel (#327). NULL for rows
|
|
509
|
+
# dispatched before #324 landed; the TUI shows "claude" as the
|
|
510
|
+
# implicit default when the column is NULL.
|
|
511
|
+
"ALTER TABLE assignments ADD COLUMN provider_name TEXT",
|
|
512
|
+
# #546: token counts for automated (claude -p) assignments, parsed from
|
|
513
|
+
# the final stream-json result event alongside cost_usd. All default
|
|
514
|
+
# 0 so the TUI can sum them without NULLs. Interactive (Max/OAuth)
|
|
515
|
+
# sessions do not bill per-token; those rows stay at 0 and the TUI
|
|
516
|
+
# labels them "Max (subscription)" rather than showing a dollar figure.
|
|
517
|
+
"ALTER TABLE assignments ADD COLUMN input_tokens INTEGER DEFAULT 0",
|
|
518
|
+
"ALTER TABLE assignments ADD COLUMN output_tokens INTEGER DEFAULT 0",
|
|
519
|
+
"ALTER TABLE assignments ADD COLUMN cache_creation_tokens INTEGER DEFAULT 0",
|
|
520
|
+
"ALTER TABLE assignments ADD COLUMN cache_read_tokens INTEGER DEFAULT 0",
|
|
521
|
+
# #546: track whether an assignment ran as a human-attended interactive
|
|
522
|
+
# session (Max/Pro subscription). Used by the TUI to show
|
|
523
|
+
# "Max (subscription)" accurately without misidentifying old automated
|
|
524
|
+
# rows that also lack cost_usd + token data.
|
|
525
|
+
"ALTER TABLE assignments ADD COLUMN is_interactive INTEGER DEFAULT 0",
|
|
526
|
+
# #618: short human-readable reason for a launch failure (e.g.
|
|
527
|
+
# "branch already checked out at ~/.coord/worktrees/<old-aid>").
|
|
528
|
+
# Written by the CLI immediately when an interactive session can't
|
|
529
|
+
# start so the TUI can explain the red box without any log file.
|
|
530
|
+
# NULL for assignments that launched successfully.
|
|
531
|
+
"ALTER TABLE assignments ADD COLUMN failure_reason TEXT",
|
|
532
|
+
# #776 (Merge Queue v2-A): track when an entry was added to the queue
|
|
533
|
+
# so the merge plan can display age and sort stably. NULL for entries
|
|
534
|
+
# created before this migration ran.
|
|
535
|
+
"ALTER TABLE merge_queue ADD COLUMN enqueued_at REAL",
|
|
536
|
+
# #821: commit-bound review gate — SHA of the branch HEAD at the time
|
|
537
|
+
# the review assignment ran. When both this column and the merge-queue
|
|
538
|
+
# entry's branch_head_sha are populated and differ, `has_approved_review`
|
|
539
|
+
# treats the approval as stale (new commits since the review → re-review
|
|
540
|
+
# required). NULL for rows predating this feature.
|
|
541
|
+
"ALTER TABLE assignments ADD COLUMN review_head_sha TEXT",
|
|
542
|
+
# #944: the Acceptance-gate verdict (oracle loop, docs/ORACLE_LOOP.md)
|
|
543
|
+
# — set by `coord acceptance record --issue N --sha <sha>`, the
|
|
544
|
+
# coordinator's external re-run of the sealed suite against the
|
|
545
|
+
# pushed SHA. NULL until a record has run. acceptance_reason carries
|
|
546
|
+
# a short failing-test summary (mirrors test_reason); acceptance_sha
|
|
547
|
+
# is the exact commit the verdict was recorded against, so staleness
|
|
548
|
+
# (new commits since the last record) is detectable the same way
|
|
549
|
+
# review_head_sha detects a stale review approval.
|
|
550
|
+
"ALTER TABLE assignments ADD COLUMN acceptance_state TEXT",
|
|
551
|
+
"ALTER TABLE assignments ADD COLUMN acceptance_reason TEXT",
|
|
552
|
+
"ALTER TABLE assignments ADD COLUMN acceptance_sha TEXT",
|
|
553
|
+
# #932: per-test counts alongside acceptance_state, so the Acceptance
|
|
554
|
+
# box can render "3/7 acceptance green" instead of a bare verdict.
|
|
555
|
+
"ALTER TABLE assignments ADD COLUMN acceptance_total INTEGER",
|
|
556
|
+
"ALTER TABLE assignments ADD COLUMN acceptance_passed INTEGER",
|
|
557
|
+
# #874: persist the worker's ### Summary prose block so the TUI's
|
|
558
|
+
# Summary tab has a durable, board-sourced field. NULL when the
|
|
559
|
+
# worker emitted no summary (best-effort; never blocks completion).
|
|
560
|
+
"ALTER TABLE assignments ADD COLUMN completion_summary TEXT",
|
|
561
|
+
# #886 Phase 2: structured Milestone Outcome Audit verdict. Written by
|
|
562
|
+
# issue_store._persist_audit_result for type="audit" assignments only
|
|
563
|
+
# (the epic's issue_number doubles as the audit's issue_number — see
|
|
564
|
+
# #885's _dispatch_audit_of). audit_goals_json is a JSON array of
|
|
565
|
+
# {goal, metric_before, metric_after, verdict (met|partial|gap),
|
|
566
|
+
# evidence} — kept as a raw JSON string on the wire, same convention
|
|
567
|
+
# as review_findings above. audit_run_number increments once per
|
|
568
|
+
# `--audit-of <epic>` run against the same (repo_name, issue_number)
|
|
569
|
+
# so later runs can diff against earlier ones. NULL for every row
|
|
570
|
+
# predating this feature and for non-audit assignment types.
|
|
571
|
+
"ALTER TABLE assignments ADD COLUMN audit_goals_json TEXT",
|
|
572
|
+
"ALTER TABLE assignments ADD COLUMN audit_bottom_line TEXT",
|
|
573
|
+
"ALTER TABLE assignments ADD COLUMN audit_run_number INTEGER",
|
|
574
|
+
# #1077: the originating assignment's `type` (e.g. "work",
|
|
575
|
+
# "mock-author"), so the merge processor can tell whether merging
|
|
576
|
+
# this entry's PR should deterministically close `issue_number` —
|
|
577
|
+
# see coord.models.CLOSES_ISSUE_TYPES. Existing rows default to
|
|
578
|
+
# 'work', preserving the prior always-close behavior for entries
|
|
579
|
+
# enqueued before this column existed.
|
|
580
|
+
"ALTER TABLE merge_queue ADD COLUMN assignment_type TEXT DEFAULT 'work'",
|
|
581
|
+
# #1084: for type="test-author" JIT-mode assignments, the work-order
|
|
582
|
+
# member issue this dispatch is extending the acceptance suite for —
|
|
583
|
+
# see coord.models.Assignment.for_issue_number. NULL for milestone-
|
|
584
|
+
# mode (Gate A) authoring, every other assignment type, and rows
|
|
585
|
+
# predating this column.
|
|
586
|
+
"ALTER TABLE assignments ADD COLUMN for_issue_number INTEGER",
|
|
587
|
+
# #1213: snapshot of the originating assignment's resolved
|
|
588
|
+
# required_gates (JSON array; NULL/'[]' = no per-issue override —
|
|
589
|
+
# callers fall back to config.pipeline.default_gates), captured at
|
|
590
|
+
# enqueue time so the review/smoke merge gates are commit-bound
|
|
591
|
+
# rather than re-resolved from the live board at merge time. NULL
|
|
592
|
+
# for rows predating this column, which fall back the same way.
|
|
593
|
+
"ALTER TABLE merge_queue ADD COLUMN required_gates TEXT",
|
|
594
|
+
# #1456: audit trail for a coordinator override of a reviewer's verdict
|
|
595
|
+
# (the #476 approve-with-nits gate). `review_verdict_original` holds
|
|
596
|
+
# the reviewer's own verdict and `review_verdict_override_reason` the
|
|
597
|
+
# parsed counts that justified the override; `review_verdict` keeps the
|
|
598
|
+
# effective value the merge gate reads. NULL for every row where the
|
|
599
|
+
# coordinator never overrode anything, and for rows predating this
|
|
600
|
+
# column — see coord.models.Assignment.review_verdict_original.
|
|
601
|
+
"ALTER TABLE assignments ADD COLUMN review_verdict_original TEXT",
|
|
602
|
+
"ALTER TABLE assignments ADD COLUMN review_verdict_override_reason TEXT",
|
|
603
|
+
# #1475: content-addressed fingerprint of the diff a review approved
|
|
604
|
+
# (`git patch-id --stable`), captured alongside `review_head_sha`. A
|
|
605
|
+
# rebase that changes no content produces the same patch-id even
|
|
606
|
+
# though the branch's HEAD SHA moved, so `has_approved_review` can
|
|
607
|
+
# carry the approval forward instead of re-blocking on the #821 SHA
|
|
608
|
+
# check. NULL for rows predating this column or where the patch-id
|
|
609
|
+
# could not be computed — those fall back to today's SHA-only
|
|
610
|
+
# staleness behaviour (fail closed).
|
|
611
|
+
"ALTER TABLE assignments ADD COLUMN review_patch_id TEXT",
|
|
612
|
+
# #1479: Test-gate staleness anchor, captured (best-effort) alongside
|
|
613
|
+
# a terminal test_state write in
|
|
614
|
+
# `coord.state._record_test_verdict_local`. `test_head_sha` /
|
|
615
|
+
# `test_patch_id` mirror `review_head_sha` / `review_patch_id`
|
|
616
|
+
# (#821/#1475) for the branch's own content; `test_base_sha` is the
|
|
617
|
+
# NEW piece — the target/merge branch's HEAD SHA at test time, so
|
|
618
|
+
# `coord.merge_queue.has_smoke_verdict` can detect a base that moved
|
|
619
|
+
# out from under an otherwise content-identical branch (a rebase can
|
|
620
|
+
# break tests without changing the branch's own diff). NULL for rows
|
|
621
|
+
# predating this column or where the anchor couldn't be computed —
|
|
622
|
+
# those fall back to today's no-staleness-check behavior (fail open).
|
|
623
|
+
"ALTER TABLE assignments ADD COLUMN test_head_sha TEXT",
|
|
624
|
+
"ALTER TABLE assignments ADD COLUMN test_patch_id TEXT",
|
|
625
|
+
"ALTER TABLE assignments ADD COLUMN test_base_sha TEXT",
|
|
626
|
+
# #1476: audit trail for a SCOPED re-review — dispatched when a
|
|
627
|
+
# conflict-fix rebase changed content under an already-approved
|
|
628
|
+
# review (patch-id mismatch) but no other work/fix commit
|
|
629
|
+
# intervened. `review_scoped` marks the review row as scoped (vs. a
|
|
630
|
+
# full re-review of the whole PR); `review_scope_base_sha` records
|
|
631
|
+
# the prior review's `review_head_sha` — the commit the resolution
|
|
632
|
+
# delta was computed from — so a later audit can distinguish
|
|
633
|
+
# "reviewed the 15-line resolution" from "reviewed the whole PR".
|
|
634
|
+
# 0/NULL for every row predating this column and for ordinary
|
|
635
|
+
# (non-scoped) reviews.
|
|
636
|
+
"ALTER TABLE assignments ADD COLUMN review_scoped INTEGER DEFAULT 0",
|
|
637
|
+
"ALTER TABLE assignments ADD COLUMN review_scope_base_sha TEXT",
|
|
638
|
+
# #1499: durable provenance — `f"drive:{repo}#{issue}"` when this
|
|
639
|
+
# assignment was dispatched by `coord drive` (via `coord assign
|
|
640
|
+
# --driven-by`), NULL for a hand `coord assign` and for every row
|
|
641
|
+
# predating this column. This is the piece that survives the
|
|
642
|
+
# driver process exiting — see coord.models.Assignment.driven_by.
|
|
643
|
+
"ALTER TABLE assignments ADD COLUMN driven_by TEXT",
|
|
644
|
+
# #1629 (H-2): the toolchain string that produced a Test-gate
|
|
645
|
+
# verdict — e.g. "rustc 1.95.0" or "python 3.12.4, node 20.11.0" —
|
|
646
|
+
# captured (best-effort) alongside `test_state` in
|
|
647
|
+
# `coord.state._record_test_verdict_local`. Annotation only: nothing
|
|
648
|
+
# reads this to gate dispatch/review/merge (see
|
|
649
|
+
# coord.health.checks.toolchain's fleet_toolchain_skew, which is the
|
|
650
|
+
# thing that actually judges skew). NULL for every row predating
|
|
651
|
+
# this column and for any verdict recorded without a resolvable
|
|
652
|
+
# toolchain — a historical/unknown value, never treated as a mismatch.
|
|
653
|
+
"ALTER TABLE assignments ADD COLUMN test_toolchain TEXT",
|
|
654
|
+
# #1757 (deploy gates): mark a queue entry so that when it completes,
|
|
655
|
+
# the tick STOPS launching and waits for a human deploy step —
|
|
656
|
+
# `merged != live` is this repo's most-repeated operational lesson and
|
|
657
|
+
# a queue that models merge but not deploy sequences work straight into
|
|
658
|
+
# it, unattended. DQ-1 shipped before this, so an existing
|
|
659
|
+
# ~/.coord/coord.db is upgraded in place here (the same columns are in
|
|
660
|
+
# `_ensure_schema`'s CREATE for fresh databases; the duplicate-column
|
|
661
|
+
# OperationalError is swallowed below).
|
|
662
|
+
#
|
|
663
|
+
# hold_after 0/1 — this entry ends with a deploy gate
|
|
664
|
+
# hold_reason shown to the operator when the gate fires
|
|
665
|
+
# resume_when optional probe command; '' = manual resume only
|
|
666
|
+
# hold_state ''|armed|fired|released (coord/drive_queue.py)
|
|
667
|
+
# hold_probes consecutive failed `resume_when` runs since the gate
|
|
668
|
+
# fired — a TYPED count, so the alert's rising attempt
|
|
669
|
+
# number never has to be parsed back out of prose
|
|
670
|
+
# (#1523 §2: typed state, never CLI prose).
|
|
671
|
+
"ALTER TABLE drive_queue ADD COLUMN hold_after INTEGER NOT NULL DEFAULT 0",
|
|
672
|
+
"ALTER TABLE drive_queue ADD COLUMN hold_reason TEXT NOT NULL DEFAULT ''",
|
|
673
|
+
"ALTER TABLE drive_queue ADD COLUMN resume_when TEXT NOT NULL DEFAULT ''",
|
|
674
|
+
"ALTER TABLE drive_queue ADD COLUMN hold_state TEXT NOT NULL DEFAULT ''",
|
|
675
|
+
"ALTER TABLE drive_queue ADD COLUMN hold_probes INTEGER NOT NULL DEFAULT 0",
|
|
676
|
+
# #1870: the short hostname of the machine that actually launched this
|
|
677
|
+
# entry's `coord drive --tmux` — see the CREATE TABLE comment above.
|
|
678
|
+
# '' for every row written before this migration; `_reconcile_running`
|
|
679
|
+
# treats that exactly like "launched here" (today's behaviour).
|
|
680
|
+
"ALTER TABLE drive_queue ADD COLUMN launch_host TEXT NOT NULL DEFAULT ''",
|
|
681
|
+
# #1892: count of automatic `CiStore.rerun_for_pr` calls `merge_queue
|
|
682
|
+
# .process()` has issued for this entry's current verdictless-CI-
|
|
683
|
+
# failure streak — see `coord.merge_queue.MAX_CI_INFRA_RERUNS` and
|
|
684
|
+
# `QueuedMerge.ci_infra_reruns`'s docstring. 0 for every row
|
|
685
|
+
# predating this column (no auto-reruns spent yet), same as the
|
|
686
|
+
# column's own default for freshly-enqueued entries.
|
|
687
|
+
"ALTER TABLE merge_queue ADD COLUMN ci_infra_reruns INTEGER NOT NULL DEFAULT 0",
|
|
688
|
+
# #1956: verdict provenance — WHO recorded `review_verdict` and HOW
|
|
689
|
+
# (see coord.models.Assignment.verdict_source for the three values
|
|
690
|
+
# and why conflating them was the second half of #1956). NULL for
|
|
691
|
+
# every row predating this column and for the common case (the
|
|
692
|
+
# reviewer's own log was parsed) — callers treat NULL identically to
|
|
693
|
+
# "agent".
|
|
694
|
+
"ALTER TABLE assignments ADD COLUMN verdict_source TEXT",
|
|
695
|
+
"ALTER TABLE assignments ADD COLUMN verdict_source_reason TEXT",
|
|
696
|
+
]
|
|
697
|
+
for sql in migrations:
|
|
698
|
+
try:
|
|
699
|
+
conn.execute(sql)
|
|
700
|
+
conn.commit()
|
|
701
|
+
except sqlite3.OperationalError:
|
|
702
|
+
pass # Column already exists
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# ── JSON migration ─────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
def _maybe_migrate_json(conn: sqlite3.Connection) -> None:
|
|
708
|
+
"""Migrate old JSON files to SQLite if dispatched.json exists and DB is empty.
|
|
709
|
+
|
|
710
|
+
The ``json_migrated`` marker in board_meta is checked first. Once set it
|
|
711
|
+
persists forever, so migration never re-runs even if JSON files reappear
|
|
712
|
+
(e.g. from stale code, test fixtures, or an agent writing legacy state).
|
|
713
|
+
"""
|
|
714
|
+
# Marker check must come first — bail out immediately if migration already ran.
|
|
715
|
+
cursor = conn.execute(
|
|
716
|
+
"SELECT value FROM board_meta WHERE key='json_migrated'"
|
|
717
|
+
)
|
|
718
|
+
if cursor.fetchone() is not None:
|
|
719
|
+
return
|
|
720
|
+
dispatched_json = COORD_DIR / "dispatched.json"
|
|
721
|
+
if not dispatched_json.exists():
|
|
722
|
+
return
|
|
723
|
+
cursor = conn.execute("SELECT COUNT(*) FROM assignments")
|
|
724
|
+
if cursor.fetchone()[0] > 0:
|
|
725
|
+
return
|
|
726
|
+
try:
|
|
727
|
+
_migrate_json(conn)
|
|
728
|
+
except Exception as exc: # noqa: BLE001 — migration is best-effort
|
|
729
|
+
import sys
|
|
730
|
+
print(f"coord: warning: JSON→SQLite migration failed: {exc}", file=sys.stderr)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _migrate_json(conn: sqlite3.Connection) -> None: # noqa: C901 — acceptable complexity
|
|
734
|
+
"""One-shot migration from JSON files to SQLite. Renames JSON files to .bak."""
|
|
735
|
+
import time as _time
|
|
736
|
+
|
|
737
|
+
dispatched_json = COORD_DIR / "dispatched.json"
|
|
738
|
+
notified_json = COORD_DIR / "notified.json"
|
|
739
|
+
board_json = COORD_DIR / "board.json"
|
|
740
|
+
proposals_json = COORD_DIR / "pending_proposals.json"
|
|
741
|
+
splits_json = COORD_DIR / "pending_splits.json"
|
|
742
|
+
plans_json = COORD_DIR / "plans.json"
|
|
743
|
+
session_json = COORD_DIR / "session.json"
|
|
744
|
+
merge_queue_json = COORD_DIR / "merge_queue.json"
|
|
745
|
+
|
|
746
|
+
with conn: # single transaction
|
|
747
|
+
# 1. dispatched.json → assignments (initial insert, status='running')
|
|
748
|
+
dispatched_data: list[dict] = []
|
|
749
|
+
if dispatched_json.exists():
|
|
750
|
+
try:
|
|
751
|
+
dispatched_data = json.loads(dispatched_json.read_text())
|
|
752
|
+
except Exception: # noqa: BLE001
|
|
753
|
+
pass
|
|
754
|
+
for rec in dispatched_data:
|
|
755
|
+
conn.execute(
|
|
756
|
+
"""INSERT OR IGNORE INTO assignments (
|
|
757
|
+
assignment_id, machine_name, repo_name, repo_github,
|
|
758
|
+
issue_number, issue_title, status, type, briefing,
|
|
759
|
+
files_allowed, model, dispatched_at, review_of_assignment_id,
|
|
760
|
+
required_gates
|
|
761
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?, ?)""",
|
|
762
|
+
(
|
|
763
|
+
rec.get("assignment_id", ""),
|
|
764
|
+
rec.get("machine_name", ""),
|
|
765
|
+
rec.get("repo_name", ""),
|
|
766
|
+
rec.get("repo_github"),
|
|
767
|
+
rec.get("issue_number", 0),
|
|
768
|
+
rec.get("issue_title", ""),
|
|
769
|
+
rec.get("type", "work"),
|
|
770
|
+
rec.get("briefing", ""),
|
|
771
|
+
json.dumps(rec.get("files_likely", [])),
|
|
772
|
+
rec.get("model"),
|
|
773
|
+
rec.get("dispatched_at"),
|
|
774
|
+
rec.get("review_of_assignment_id"),
|
|
775
|
+
json.dumps(rec.get("required_gates", [])),
|
|
776
|
+
),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# 2. notified.json → notifications
|
|
780
|
+
if notified_json.exists():
|
|
781
|
+
try:
|
|
782
|
+
notified: dict[str, dict] = json.loads(notified_json.read_text())
|
|
783
|
+
for aid, info in notified.items():
|
|
784
|
+
conn.execute(
|
|
785
|
+
"""INSERT OR REPLACE INTO notifications
|
|
786
|
+
(assignment_id, event, branch, posted_at)
|
|
787
|
+
VALUES (?, ?, ?, ?)""",
|
|
788
|
+
(aid, info.get("event", ""), info.get("branch"),
|
|
789
|
+
info.get("posted_at", _time.time())),
|
|
790
|
+
)
|
|
791
|
+
except Exception: # noqa: BLE001
|
|
792
|
+
pass
|
|
793
|
+
|
|
794
|
+
# 3. board.json → assignments (richer status fields via REPLACE)
|
|
795
|
+
if board_json.exists():
|
|
796
|
+
try:
|
|
797
|
+
board_data = json.loads(board_json.read_text())
|
|
798
|
+
all_entries = (
|
|
799
|
+
board_data.get("active", []) + board_data.get("completed", [])
|
|
800
|
+
)
|
|
801
|
+
for a in all_entries:
|
|
802
|
+
aid = a.get("assignment_id")
|
|
803
|
+
if not aid:
|
|
804
|
+
continue
|
|
805
|
+
conn.execute(
|
|
806
|
+
"""INSERT OR REPLACE INTO assignments (
|
|
807
|
+
assignment_id, machine_name, repo_name, repo_github,
|
|
808
|
+
issue_number, issue_title, status, type, branch, pr_url,
|
|
809
|
+
briefing, files_allowed, files_forbidden, model,
|
|
810
|
+
dispatched_at, finished_at, smoke_test, smoke_test_reason,
|
|
811
|
+
review_state, review_of_assignment_id, review_target,
|
|
812
|
+
required_gates, plan, unreachable_count, exit_code
|
|
813
|
+
) VALUES (
|
|
814
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
815
|
+
?, ?, ?, ?, ?, ?, ?, ?,
|
|
816
|
+
?, ?, ?, ?, ?, ?, ?
|
|
817
|
+
)""",
|
|
818
|
+
(
|
|
819
|
+
aid,
|
|
820
|
+
a.get("machine_name", ""),
|
|
821
|
+
a.get("repo_name", ""),
|
|
822
|
+
None, # repo_github not in board JSON
|
|
823
|
+
a.get("issue_number", 0),
|
|
824
|
+
a.get("issue_title", ""),
|
|
825
|
+
a.get("status", "running"),
|
|
826
|
+
a.get("type", "work"),
|
|
827
|
+
a.get("branch"),
|
|
828
|
+
a.get("pr_url"),
|
|
829
|
+
a.get("briefing", ""),
|
|
830
|
+
json.dumps(a.get("files_allowed", [])),
|
|
831
|
+
json.dumps(a.get("files_forbidden", [])),
|
|
832
|
+
a.get("model"),
|
|
833
|
+
a.get("dispatched_at"),
|
|
834
|
+
a.get("finished_at"),
|
|
835
|
+
a.get("smoke_test"),
|
|
836
|
+
a.get("smoke_test_reason"),
|
|
837
|
+
a.get("review_state"),
|
|
838
|
+
a.get("review_of_assignment_id"),
|
|
839
|
+
a.get("review_target"),
|
|
840
|
+
json.dumps(a.get("required_gates", [])),
|
|
841
|
+
json.dumps(a.get("plan")) if a.get("plan") else None,
|
|
842
|
+
a.get("unreachable_count", 0),
|
|
843
|
+
a.get("exit_code"),
|
|
844
|
+
),
|
|
845
|
+
)
|
|
846
|
+
round_number = board_data.get("round_number", 0)
|
|
847
|
+
conn.execute(
|
|
848
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES ('round_number', ?)",
|
|
849
|
+
(str(round_number),),
|
|
850
|
+
)
|
|
851
|
+
conn.execute(
|
|
852
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES ('board_initialized', '1')"
|
|
853
|
+
)
|
|
854
|
+
except Exception: # noqa: BLE001
|
|
855
|
+
pass
|
|
856
|
+
|
|
857
|
+
# 4. proposals
|
|
858
|
+
if proposals_json.exists():
|
|
859
|
+
try:
|
|
860
|
+
for p in json.loads(proposals_json.read_text()):
|
|
861
|
+
conn.execute(
|
|
862
|
+
"""INSERT OR IGNORE INTO proposals
|
|
863
|
+
(id, machine_name, repo_name, issue_number, issue_title,
|
|
864
|
+
rationale, files_likely, briefing, model, type, required_gates)
|
|
865
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
866
|
+
(
|
|
867
|
+
p.get("id"), p.get("machine_name", ""), p.get("repo_name", ""),
|
|
868
|
+
p.get("issue_number", 0), p.get("issue_title", ""),
|
|
869
|
+
p.get("rationale", ""), json.dumps(p.get("files_likely", [])),
|
|
870
|
+
p.get("briefing", ""), p.get("model"),
|
|
871
|
+
p.get("type", "work"), json.dumps(p.get("required_gates", [])),
|
|
872
|
+
),
|
|
873
|
+
)
|
|
874
|
+
except Exception: # noqa: BLE001
|
|
875
|
+
pass
|
|
876
|
+
|
|
877
|
+
# 5. split proposals + chunks
|
|
878
|
+
if splits_json.exists():
|
|
879
|
+
try:
|
|
880
|
+
for s in json.loads(splits_json.read_text()):
|
|
881
|
+
conn.execute(
|
|
882
|
+
"""INSERT OR IGNORE INTO split_proposals
|
|
883
|
+
(id, repo_name, issue_number, issue_title, rationale)
|
|
884
|
+
VALUES (?, ?, ?, ?, ?)""",
|
|
885
|
+
(s.get("id"), s.get("repo_name", ""), s.get("issue_number", 0),
|
|
886
|
+
s.get("issue_title", ""), s.get("rationale", "")),
|
|
887
|
+
)
|
|
888
|
+
for chunk in s.get("chunks", []):
|
|
889
|
+
conn.execute(
|
|
890
|
+
"""INSERT INTO split_chunks
|
|
891
|
+
(split_proposal_id, title, scope, files_likely)
|
|
892
|
+
VALUES (?, ?, ?, ?)""",
|
|
893
|
+
(s.get("id"), chunk.get("title", ""), chunk.get("scope", ""),
|
|
894
|
+
json.dumps(chunk.get("files_likely", []))),
|
|
895
|
+
)
|
|
896
|
+
except Exception: # noqa: BLE001
|
|
897
|
+
pass
|
|
898
|
+
|
|
899
|
+
# 6. plans
|
|
900
|
+
if plans_json.exists():
|
|
901
|
+
try:
|
|
902
|
+
for aid, plan_dict in json.loads(plans_json.read_text()).items():
|
|
903
|
+
conn.execute(
|
|
904
|
+
"INSERT OR IGNORE INTO plans (assignment_id, plan_data) VALUES (?, ?)",
|
|
905
|
+
(aid, json.dumps(plan_dict)),
|
|
906
|
+
)
|
|
907
|
+
except Exception: # noqa: BLE001
|
|
908
|
+
pass
|
|
909
|
+
|
|
910
|
+
# 7. session
|
|
911
|
+
if session_json.exists():
|
|
912
|
+
try:
|
|
913
|
+
sess = json.loads(session_json.read_text())
|
|
914
|
+
conn.execute(
|
|
915
|
+
"""INSERT INTO sessions
|
|
916
|
+
(started_at, ended_at, clean_shutdown,
|
|
917
|
+
completed_this_session, issues_closed, total_cost_usd)
|
|
918
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
919
|
+
(
|
|
920
|
+
sess.get("started_at"),
|
|
921
|
+
sess.get("ended_at"),
|
|
922
|
+
1 if sess.get("clean_shutdown") else 0,
|
|
923
|
+
json.dumps(sess.get("completed_this_session", [])),
|
|
924
|
+
json.dumps(sess.get("issues_closed", [])),
|
|
925
|
+
sess.get("total_cost_usd"),
|
|
926
|
+
),
|
|
927
|
+
)
|
|
928
|
+
except Exception: # noqa: BLE001
|
|
929
|
+
pass
|
|
930
|
+
|
|
931
|
+
# 8. merge queue
|
|
932
|
+
if merge_queue_json.exists():
|
|
933
|
+
try:
|
|
934
|
+
for entry in json.loads(merge_queue_json.read_text()):
|
|
935
|
+
conn.execute(
|
|
936
|
+
"""INSERT INTO merge_queue (
|
|
937
|
+
assignment_id, repo_name, repo_github, branch,
|
|
938
|
+
target_branch, issue_number, issue_title, state,
|
|
939
|
+
pr_number, pr_url, size, last_attempt, error
|
|
940
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
941
|
+
(
|
|
942
|
+
entry.get("assignment_id"), entry.get("repo_name"),
|
|
943
|
+
entry.get("repo_github"), entry.get("branch"),
|
|
944
|
+
entry.get("target_branch"), entry.get("issue_number"),
|
|
945
|
+
entry.get("issue_title"), entry.get("state", "pending"),
|
|
946
|
+
entry.get("pr_number"), entry.get("pr_url"),
|
|
947
|
+
entry.get("size"), entry.get("last_attempt"),
|
|
948
|
+
entry.get("error"),
|
|
949
|
+
),
|
|
950
|
+
)
|
|
951
|
+
except Exception: # noqa: BLE001
|
|
952
|
+
pass
|
|
953
|
+
|
|
954
|
+
# Persist migration marker — this is the canonical "already migrated" signal.
|
|
955
|
+
# Checked at the top of _maybe_migrate_json(), so JSON files reappearing
|
|
956
|
+
# later (stale code, test fixtures, agent writing legacy state) won't
|
|
957
|
+
# re-trigger the migration.
|
|
958
|
+
conn.execute(
|
|
959
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES ('json_migrated', ?)",
|
|
960
|
+
(str(_time.time()),),
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Rename JSON files to .bak so migration doesn't re-run
|
|
964
|
+
for f in [
|
|
965
|
+
dispatched_json, notified_json, board_json, proposals_json,
|
|
966
|
+
splits_json, plans_json, session_json, merge_queue_json,
|
|
967
|
+
]:
|
|
968
|
+
if f.exists():
|
|
969
|
+
try:
|
|
970
|
+
f.rename(f.with_suffix(f.suffix + ".bak"))
|
|
971
|
+
except Exception: # noqa: BLE001
|
|
972
|
+
pass
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
# ── Gate-order migration (Test-before-Review reorder) ─────────────────────────
|
|
976
|
+
|
|
977
|
+
def _migrate_gate_order(conn: sqlite3.Connection) -> None:
|
|
978
|
+
"""Rewrite the stale default gate order in stored rows.
|
|
979
|
+
|
|
980
|
+
The default gate order moved Test ahead of Review — from the #520-era
|
|
981
|
+
``["review", "test", "merge"]`` to ``["test", "review", "merge"]`` — now
|
|
982
|
+
that the agent-assisted Testing stage is smooth enough to run as a smoke
|
|
983
|
+
test *before* the PR/review (the natural order), reversing #520's "get the
|
|
984
|
+
review over with first" workaround. Any assignment, proposal, or
|
|
985
|
+
``board_meta`` row still carrying the previous implicit default is updated
|
|
986
|
+
so the pipeline display and the headless review gate
|
|
987
|
+
(:meth:`PipelineConfig.test_precedes_review`) agree. Only the exact
|
|
988
|
+
previous-default JSON string is touched — user-customised gate lists
|
|
989
|
+
(anything else) are left unchanged.
|
|
990
|
+
|
|
991
|
+
The target string (``["test", "review", "merge"]``) is also the pre-#520
|
|
992
|
+
original, so a DB that never migrated forward is already in the desired
|
|
993
|
+
state and untouched.
|
|
994
|
+
|
|
995
|
+
This function is idempotent: if the previous-default string is absent, no
|
|
996
|
+
rows change.
|
|
997
|
+
"""
|
|
998
|
+
_OLD = '["review", "test", "merge"]'
|
|
999
|
+
_NEW = '["test", "review", "merge"]'
|
|
1000
|
+
conn.execute(
|
|
1001
|
+
"UPDATE assignments SET required_gates = ? WHERE required_gates = ?",
|
|
1002
|
+
(_NEW, _OLD),
|
|
1003
|
+
)
|
|
1004
|
+
conn.execute(
|
|
1005
|
+
"UPDATE proposals SET required_gates = ? WHERE required_gates = ?",
|
|
1006
|
+
(_NEW, _OLD),
|
|
1007
|
+
)
|
|
1008
|
+
conn.execute(
|
|
1009
|
+
"UPDATE board_meta SET value = ? "
|
|
1010
|
+
"WHERE key = 'pipeline_default_gates' AND value = ?",
|
|
1011
|
+
(_NEW, _OLD),
|
|
1012
|
+
)
|
|
1013
|
+
conn.commit()
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _backfill_orphaned_review_verdicts(conn: sqlite3.Connection) -> int:
|
|
1017
|
+
"""#1663: copy a captured review verdict onto its parent WORK row.
|
|
1018
|
+
|
|
1019
|
+
``run_drain`` (#1616) captured the verdict onto the ``type='review'`` row
|
|
1020
|
+
and never propagated it to the parent, because the only path to that write
|
|
1021
|
+
ran through ``auto_loop.process_review_completion`` — which also dispatches
|
|
1022
|
+
fix workers, so the daemon's clock refused to enter it at all. Every
|
|
1023
|
+
verdict the drain consumed instead of a human's ``coord notify`` therefore
|
|
1024
|
+
left its work row at ``review_state='dispatched'`` / ``review_verdict``
|
|
1025
|
+
NULL. The code fix is in ``coord.notify._run_drain_locked`` +
|
|
1026
|
+
``coord.auto_loop.propagate_review_verdict``; this repairs the rows already
|
|
1027
|
+
stranded when it landed (the 2026-08-01 batch — #1527 #1624 #1658 #1633
|
|
1028
|
+
#1353 — plus #544, #1078 and #1122), so none of them needs a re-review at
|
|
1029
|
+
$1-3 a head.
|
|
1030
|
+
|
|
1031
|
+
**Copies only.** The verdict is read from the existing review row via
|
|
1032
|
+
``review_of_assignment_id``; nothing is synthesised, re-derived from a
|
|
1033
|
+
findings body, or inferred from pipeline state. A work row whose review
|
|
1034
|
+
row never captured a verdict (#1122's ``188ae219aca3``, lost to #1636/#1658
|
|
1035
|
+
and recovered by hand onto PR #1656) has nothing to copy and is left
|
|
1036
|
+
exactly as it is — a fabricated verdict there would overwrite hand-recovered
|
|
1037
|
+
findings with a guess.
|
|
1038
|
+
|
|
1039
|
+
Scoped to work rows that are actually stranded: a work-like ``type``, a NULL
|
|
1040
|
+
``review_verdict`` of their own, and a review stage that demonstrably ran
|
|
1041
|
+
(``review_state`` in ``dispatched``/``done``). Rows at ``pending`` /
|
|
1042
|
+
``advisory`` / NULL are untouched — their review hasn't run or was waived.
|
|
1043
|
+
|
|
1044
|
+
Idempotent, and runs on every connection open like
|
|
1045
|
+
:func:`_migrate_gate_order`: after the first pass every candidate has a
|
|
1046
|
+
non-NULL ``review_verdict``, so the ``UPDATE`` matches nothing. Returns the
|
|
1047
|
+
number of rows repaired (0 on the steady state) for logging/tests.
|
|
1048
|
+
"""
|
|
1049
|
+
# Latest verdict-carrying review for this work row. `dispatched_at DESC`
|
|
1050
|
+
# picks the most recent round when a work row was reviewed more than once.
|
|
1051
|
+
# Deliberately NOT filtered on `r.status='done'`: #1122's review row is
|
|
1052
|
+
# `failed`, and a verdict recovered onto a failed row by hand (#617's
|
|
1053
|
+
# transcript recovery, `coord diagnose --stage review`) was still earned by
|
|
1054
|
+
# a real review and is exactly what we want to preserve.
|
|
1055
|
+
_LATEST_VERDICT = """
|
|
1056
|
+
SELECT r.review_verdict FROM assignments r
|
|
1057
|
+
WHERE r.review_of_assignment_id = assignments.assignment_id
|
|
1058
|
+
AND r.type = 'review'
|
|
1059
|
+
AND r.review_verdict IS NOT NULL
|
|
1060
|
+
AND r.review_verdict != ''
|
|
1061
|
+
ORDER BY r.dispatched_at DESC
|
|
1062
|
+
LIMIT 1
|
|
1063
|
+
"""
|
|
1064
|
+
cur = conn.execute(
|
|
1065
|
+
f"""
|
|
1066
|
+
UPDATE assignments
|
|
1067
|
+
SET review_verdict = ({_LATEST_VERDICT}),
|
|
1068
|
+
review_state = 'done'
|
|
1069
|
+
WHERE type IN ('work', 'mock-author', 'test-author')
|
|
1070
|
+
AND review_verdict IS NULL
|
|
1071
|
+
AND review_state IN ('dispatched', 'done')
|
|
1072
|
+
AND ({_LATEST_VERDICT}) IS NOT NULL
|
|
1073
|
+
"""
|
|
1074
|
+
)
|
|
1075
|
+
conn.commit()
|
|
1076
|
+
return cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
|