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.
Files changed (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
@@ -0,0 +1,229 @@
1
+ """Storage-neutral row → dataclass mapping for the board.
2
+
3
+ Extracted from :mod:`coord.state` (#584 portable control center) so that both
4
+ the local SQLite path (``coord.state``) and the networked daemon path
5
+ (``coord.dao`` / ``coord.serve_app`` serving the board, and ``coord.client``
6
+ reconstructing it from JSON) share **one** implementation — no Python-vs-Python
7
+ drift between the locally-built board and the daemon-served board.
8
+
9
+ The helpers are deliberately tolerant of two input shapes:
10
+
11
+ * a ``sqlite3.Row`` (or any dict-like) whose JSON columns are still raw strings
12
+ (the local SQLite path), and
13
+ * a plain ``dict`` decoded from the daemon's JSON ``/board`` payload, whose JSON
14
+ columns are already native lists/objects.
15
+
16
+ This is what lets ``row_to_assignment`` be reused verbatim on both sides.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from typing import Any
23
+
24
+ from coord.models import WORK_LIKE_TYPES, Assignment, Board
25
+
26
+ _ACTIVE_STATUSES = ("running", "pending")
27
+
28
+
29
+ def json_loads(s: Any) -> Any:
30
+ """Decode a JSON column.
31
+
32
+ Returns ``None`` for ``None``/malformed input. If *s* is already a decoded
33
+ value (not a string — e.g. a list/dict from the daemon's JSON payload) it is
34
+ returned unchanged.
35
+ """
36
+ if s is None:
37
+ return None
38
+ if not isinstance(s, (str, bytes, bytearray)):
39
+ return s # already decoded (JSON API path)
40
+ try:
41
+ return json.loads(s)
42
+ except (json.JSONDecodeError, TypeError):
43
+ return None
44
+
45
+
46
+ def decode_smoke_tests(raw: object) -> list[str] | None:
47
+ """#252: decode the ``smoke_tests`` column.
48
+
49
+ SQLite stores ``None`` (column missing / unset), ``"[]"`` (explicit "no
50
+ smoke tests — change internal") or ``'["item1", ...]'`` (bullets). Over the
51
+ daemon's JSON API the value arrives already decoded as a real list. Anything
52
+ malformed folds back to ``None`` so the TUI shows its graceful-degradation
53
+ placeholder instead of crashing.
54
+ """
55
+ if raw is None:
56
+ return None
57
+ if isinstance(raw, list):
58
+ return [str(item) for item in raw] # already decoded (JSON API path)
59
+ if not raw:
60
+ return None
61
+ try:
62
+ value = json.loads(raw)
63
+ except (json.JSONDecodeError, TypeError):
64
+ return None
65
+ if not isinstance(value, list):
66
+ return None
67
+ return [str(item) for item in value]
68
+
69
+
70
+ def row_to_assignment(row: object) -> Assignment:
71
+ """Convert a sqlite3.Row (or dict-like / decoded JSON dict) into an Assignment."""
72
+ d = dict(row)
73
+ return Assignment(
74
+ assignment_id=d.get("assignment_id"),
75
+ machine_name=d["machine_name"],
76
+ repo_name=d["repo_name"],
77
+ issue_number=d["issue_number"],
78
+ issue_title=d["issue_title"],
79
+ status=d.get("status", "running"),
80
+ type=d.get("type", "work"),
81
+ branch=d.get("branch"),
82
+ pr_url=d.get("pr_url"),
83
+ briefing=d.get("briefing") or "",
84
+ files_allowed=json_loads(d.get("files_allowed")) or [],
85
+ files_forbidden=json_loads(d.get("files_forbidden")) or [],
86
+ model=d.get("model"),
87
+ dispatched_at=d.get("dispatched_at"),
88
+ finished_at=d.get("finished_at"),
89
+ smoke_test=d.get("smoke_test"),
90
+ smoke_test_reason=d.get("smoke_test_reason"),
91
+ review_state=d.get("review_state"),
92
+ review_of_assignment_id=d.get("review_of_assignment_id"),
93
+ review_target=d.get("review_target"),
94
+ required_gates=json_loads(d.get("required_gates")) or [],
95
+ plan=json_loads(d.get("plan")),
96
+ unreachable_count=d.get("unreachable_count") or 0,
97
+ review_iteration=d.get("review_iteration") or 0,
98
+ review_posted_at=d.get("review_posted_at"),
99
+ test_state=d.get("test_state"),
100
+ test_reason=d.get("test_reason"),
101
+ # #1479: Test-gate staleness anchor; None for pre-1479 rows or where
102
+ # it couldn't be captured — the merge-queue gate treats that as "SHA
103
+ # tracking unavailable" and skips the staleness check (fail open).
104
+ test_head_sha=d.get("test_head_sha"),
105
+ test_patch_id=d.get("test_patch_id"),
106
+ test_base_sha=d.get("test_base_sha"),
107
+ # #1629: the toolchain that produced test_state; None for pre-1629
108
+ # rows or an unresolvable toolchain (renders as "unknown").
109
+ test_toolchain=d.get("test_toolchain"),
110
+ review_verdict=d.get("review_verdict"),
111
+ # #1456: coordinator-override audit trail; None when the reviewer's
112
+ # verdict stands (the normal case) and for pre-1456 rows.
113
+ review_verdict_original=d.get("review_verdict_original"),
114
+ review_verdict_override_reason=d.get("review_verdict_override_reason"),
115
+ # #821: commit-bound review gate; None for pre-821 rows.
116
+ review_head_sha=d.get("review_head_sha"),
117
+ # #1475: content-addressed patch-id alongside the SHA above; None
118
+ # for pre-1475 rows or where the patch-id couldn't be computed.
119
+ review_patch_id=d.get("review_patch_id"),
120
+ # #1476: scoped-re-review audit trail; False/None for rows predating
121
+ # this feature or ordinary (non-scoped) reviews.
122
+ review_scoped=bool(d.get("review_scoped") or False),
123
+ review_scope_base_sha=d.get("review_scope_base_sha"),
124
+ cost_usd=d.get("cost_usd"),
125
+ # #252: stored as JSON; absent column → None (not parsed yet).
126
+ smoke_tests=decode_smoke_tests(d.get("smoke_tests")),
127
+ # #324: resolved provider name; None for rows predating this feature.
128
+ provider_name=d.get("provider_name"),
129
+ # #546: token counts; absent column (pre-migration) or NULL → 0.
130
+ input_tokens=int(d.get("input_tokens") or 0),
131
+ output_tokens=int(d.get("output_tokens") or 0),
132
+ cache_creation_tokens=int(d.get("cache_creation_tokens") or 0),
133
+ cache_read_tokens=int(d.get("cache_read_tokens") or 0),
134
+ # #618: short launch-failure reason; None for successfully-launched rows.
135
+ failure_reason=d.get("failure_reason"),
136
+ # #944: Acceptance-gate verdict; None for rows predating the oracle loop
137
+ # or work that hasn't reached an `acceptance record` yet.
138
+ acceptance_state=d.get("acceptance_state"),
139
+ acceptance_reason=d.get("acceptance_reason"),
140
+ acceptance_sha=d.get("acceptance_sha"),
141
+ # #932: per-test counts; None for rows predating this column.
142
+ acceptance_total=d.get("acceptance_total"),
143
+ acceptance_passed=d.get("acceptance_passed"),
144
+ # #874: worker ### Summary prose; None when the worker emitted no block.
145
+ completion_summary=d.get("completion_summary"),
146
+ # #886 Phase 2: Milestone Outcome Audit structured verdict; None for
147
+ # non-audit assignments and for rows predating this feature.
148
+ audit_goals_json=d.get("audit_goals_json"),
149
+ audit_bottom_line=d.get("audit_bottom_line"),
150
+ audit_run_number=d.get("audit_run_number"),
151
+ # #1084: JIT test-author's per-member-issue correlation; None for
152
+ # Gate A / every other type and for rows predating this column.
153
+ for_issue_number=d.get("for_issue_number"),
154
+ # #1499: durable drive provenance; None for a hand `coord assign`
155
+ # and for rows predating this column.
156
+ driven_by=d.get("driven_by"),
157
+ # #1956: verdict provenance; None (→ treated as "agent") for every
158
+ # row predating this column and for a normally-parsed verdict.
159
+ verdict_source=d.get("verdict_source"),
160
+ verdict_source_reason=d.get("verdict_source_reason"),
161
+ )
162
+
163
+
164
+ def assemble_board(
165
+ rows: list,
166
+ plans: dict[str, dict],
167
+ round_number: object,
168
+ ) -> Board:
169
+ """Build a :class:`Board` from assignment rows + a plan map (#749).
170
+
171
+ Storage-neutral core shared by ``coord.state._query_board`` (SQLite rows)
172
+ and ``coord.client.board_from_payload`` (daemon JSON rows) — the two used
173
+ to hand-roll the identical row→``Assignment``→active/completed bucketing
174
+ loop independently, which is exactly the "dual projection" duplication
175
+ #749 set out to collapse. Deliberately does NOT call
176
+ :func:`infer_review_state` — callers that need it (both of the above) run
177
+ it themselves against their own review rows, since the two paths fetch
178
+ those from different sources (a live cursor vs. the JSON payload).
179
+ """
180
+ active: list[Assignment] = []
181
+ completed: list[Assignment] = []
182
+ for row in rows:
183
+ a = row_to_assignment(row)
184
+ if a.assignment_id and a.assignment_id in plans:
185
+ a.plan = plans[a.assignment_id]
186
+ (active if a.status in _ACTIVE_STATUSES else completed).append(a)
187
+ return Board(active=active, completed=completed, round_number=int(round_number or 0))
188
+
189
+
190
+ def infer_review_state(
191
+ board: Board,
192
+ review_rows: list,
193
+ notified_ids: set[str],
194
+ ) -> None:
195
+ """Set ``review_state`` on completed work-like assignments from their linked
196
+ reviews (#930: includes ``type="mock-author"`` — see
197
+ :data:`coord.models.WORK_LIKE_TYPES`).
198
+
199
+ Pure core of ``coord.state._infer_review_state``: takes the already-fetched
200
+ review rows (each a mapping with ``assignment_id`` /
201
+ ``review_of_assignment_id`` / ``status``) and the set of notified assignment
202
+ ids, so it can run against either a live SQLite cursor's rows or rows
203
+ reconstructed from the daemon's JSON payload.
204
+ """
205
+ review_status_for: dict[str, str] = {}
206
+ for row in review_rows:
207
+ review_status_for[row["review_of_assignment_id"]] = row["status"]
208
+
209
+ for a in board.completed:
210
+ if a.type not in WORK_LIKE_TYPES or a.assignment_id is None:
211
+ continue
212
+ if a.review_state is not None:
213
+ continue # explicitly set — don't override
214
+ review_aid = next(
215
+ (
216
+ r["assignment_id"]
217
+ for r in review_rows
218
+ if r["review_of_assignment_id"] == a.assignment_id
219
+ ),
220
+ None,
221
+ )
222
+ if review_aid is None:
223
+ continue
224
+ if review_aid in notified_ids or review_status_for.get(
225
+ a.assignment_id, ""
226
+ ) in ("done", "failed"):
227
+ a.review_state = "done"
228
+ else:
229
+ a.review_state = "dispatched"