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,252 @@
1
+ """`coord scorecard` — the dogfood scorecard (#1559): per-issue and
2
+ per-milestone evidence for "is this good enough to sell?", not just code.
3
+
4
+ **Why a new command, not an extension of ``coord usage``** (#1559 asks for
5
+ this to be justified): ``coord.usage_rollup``'s public contract (#1118) is a
6
+ *pure* cost/token/duration aggregator with no I/O and no notion of GitHub
7
+ labels or human-attended dispatch shape — that boundary is what lets
8
+ ``coord usage``, the TUI Time view, and this command all share one rollup
9
+ without three reimplementations. Grafting first-pass-acceptance,
10
+ intervention classification, and label-sourced escaped-defect/process-bug
11
+ reads onto it would blow that boundary open for a use case ``coord usage``
12
+ callers don't need. Scorecard instead *composes* three existing read seams
13
+ exactly once each, at this CLI layer, and hands the results to the pure
14
+ :mod:`coord.scorecard` aggregator:
15
+
16
+ - :func:`coord.usage.fetch_usage_rows` — the same board-row fetch
17
+ ``coord usage`` uses (daemon ``/board`` when configured, local DB else).
18
+ - :func:`coord.state.list_audit_log` — a best-effort durability cross-check
19
+ for "merged" (see :mod:`coord.scorecard`'s docstring). Paginated back to
20
+ the milestone's own creation date (see ``_AUDIT_LOG_MAX_PAGES`` below) —
21
+ a single newest-first page can't be trusted to reach an old milestone in
22
+ a repo with a long merge history.
23
+ - :func:`coord.github_ops.get_milestone_issues` — milestone membership
24
+ (open+closed) with labels, the source for the two label conventions.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json as _json
30
+ from pathlib import Path
31
+
32
+ import click
33
+
34
+ from coord.commands._common import _CONFIG_OPTION, _load_config
35
+
36
+
37
+ # The audit log is paginated at audit.MAX_LIMIT (500) per page, newest-first.
38
+ # For a repo with a long merge history, "the 500 most recent merged events"
39
+ # may not reach back to an older milestone at all — silently emptying the
40
+ # merge-durability cross-check for exactly the historical-validation case
41
+ # #1559 cares about. We bound the query with the milestone's own creation
42
+ # date (nothing can have merged for it before it existed) and paginate the
43
+ # rest of the way back to that bound, capped so one CLI invocation can't
44
+ # turn into an unbounded DB scan.
45
+ _AUDIT_LOG_PAGE_SIZE = 500
46
+ _AUDIT_LOG_MAX_PAGES = 20 # 20 * 500 = 10,000 entries
47
+
48
+
49
+ def _parse_github_timestamp(value: object) -> float | None:
50
+ """Parse a GitHub REST API ISO-8601 timestamp (e.g.
51
+ ``"2024-01-01T00:00:00Z"``) into a Unix epoch float. Returns ``None`` for
52
+ missing/malformed input so callers degrade to "no time bound" instead of
53
+ raising — the audit cross-check is best-effort end to end."""
54
+ if not value:
55
+ return None
56
+ from datetime import datetime # noqa: PLC0415
57
+
58
+ try:
59
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()
60
+ except (TypeError, ValueError):
61
+ return None
62
+
63
+
64
+ def _fmt_usd(usd: float) -> str:
65
+ return f"${usd:.2f}"
66
+
67
+
68
+ def _fmt_hms(secs: float) -> str:
69
+ total = int(round(secs))
70
+ h, rem = divmod(total, 3600)
71
+ m, s = divmod(rem, 60)
72
+ if h:
73
+ return f"{h}h{m:02d}m"
74
+ if m:
75
+ return f"{m}m{s:02d}s"
76
+ return f"{s}s"
77
+
78
+
79
+ def _fmt_rate(rate: float | None) -> str:
80
+ return "n/a" if rate is None else f"{rate * 100:.0f}%"
81
+
82
+
83
+ def _fmt_first_pass(value: str) -> str:
84
+ return {"yes": "yes", "no": "no", "unknown": "?"}.get(value, value)
85
+
86
+
87
+ def format_scorecard(card, *, verbose: bool = False) -> str:
88
+ """Human-readable rendering of a :class:`coord.scorecard.MilestoneScorecard`."""
89
+ t = card.totals
90
+ lines = [
91
+ f"DOGFOOD SCORECARD — {card.repo_name} milestone #{card.milestone} "
92
+ f"\"{card.milestone_title}\" ({t['issue_count']} issues)",
93
+ "",
94
+ f"First-pass acceptance: {t['first_pass']['yes']}/"
95
+ f"{t['first_pass']['yes'] + t['first_pass']['no']} = "
96
+ f"{_fmt_rate(t['first_pass']['rate'])}"
97
+ f" (unknown: {t['first_pass']['unknown']})",
98
+ f"Human interventions: {t['interventions']['total']}"
99
+ f" (fix={t['interventions']['by_kind']['fix']}"
100
+ f" rescue={t['interventions']['by_kind']['rescue']}"
101
+ f" nudge={t['interventions']['by_kind']['nudge']}"
102
+ f" abandon={t['interventions']['by_kind']['abandon']})"
103
+ f" [{t['interventions']['issues_with_unknown_data']} issues have no"
104
+ f" board data — excluded from the count above;"
105
+ f" {t['interventions']['issues_with_multiple_roots']} issues have a"
106
+ f" second unrelated root dispatch — first_pass=no but not counted"
107
+ " in any kind above]",
108
+ f"Cost + wall-clock: {_fmt_usd(t['cost']['total_usd'])} /"
109
+ f" {_fmt_hms(t['cost']['duration_secs'])}"
110
+ f" ({t['cost']['issues_with_data']}/{t['issue_count']} issues have"
111
+ f" cost data; {t['cost']['issues_without_data']} unknown)",
112
+ "Escaped defects: "
113
+ + " ".join(
114
+ f"{stage}={t['escaped_defects']['by_stage'][stage]}"
115
+ for stage in t["escaped_defects"]["by_stage"]
116
+ )
117
+ + f" ({t['escaped_defects']['issues_unlabeled']} issues unlabeled —"
118
+ " NOT the same as zero)",
119
+ f"Process bugs surfaced: {t['process_bugs']['count']}"
120
+ f" regression test: landed={t['process_bugs']['regression_test']['landed']}"
121
+ f" missing={t['process_bugs']['regression_test']['missing']}"
122
+ f" unknown={t['process_bugs']['regression_test']['unknown']}",
123
+ ]
124
+
125
+ if verbose:
126
+ lines.append("")
127
+ lines.append(
128
+ f"{'#':<8}{'FIRST-PASS':<11}{'INTERVENTIONS':<15}{'COST':<10}"
129
+ f"{'TIME':<9}{'ESCAPED':<12}{'PROCESS-BUG':<14}TITLE"
130
+ )
131
+ for c in card.issues:
132
+ interv = sum(c.interventions.values())
133
+ interv_str = f"{interv}" if c.has_assignment_data else "?"
134
+ if c.multiple_roots:
135
+ interv_str += "+multiroot"
136
+ cost_str = _fmt_usd(c.cost_total) if c.has_cost_data else "?"
137
+ time_str = _fmt_hms(c.duration_secs) if c.has_cost_data else "?"
138
+ escaped_str = c.escaped_defect_stage or "-"
139
+ pb_str = (
140
+ f"{c.regression_test}" if c.process_bug else "-"
141
+ )
142
+ title = c.title if len(c.title) <= 40 else c.title[:39] + "…"
143
+ lines.append(
144
+ f"#{c.number:<7}{_fmt_first_pass(c.first_pass):<11}{interv_str:<15}"
145
+ f"{cost_str:<10}{time_str:<9}{escaped_str:<12}{pb_str:<14}{title}"
146
+ )
147
+
148
+ return "\n".join(lines)
149
+
150
+
151
+ @click.command(
152
+ "scorecard",
153
+ help=(
154
+ "The dogfood scorecard (#1559): first-pass acceptance, human "
155
+ "interventions, cost + wall-clock, escaped defects, and process bugs "
156
+ "surfaced — per issue and aggregated, for one GitHub milestone.\n\n"
157
+ "REPO is the local repo name from coordinator.yml; MILESTONE is the "
158
+ "GitHub milestone NUMBER (not the tracking-issue number)."
159
+ ),
160
+ )
161
+ @click.argument("repo")
162
+ @click.argument("milestone", type=int)
163
+ @click.option(
164
+ "--verbose", "-v", is_flag=True, default=False,
165
+ help="Also print the per-issue table (human output only; --json always includes it).",
166
+ )
167
+ @click.option("--json", "output_json", is_flag=True, default=False, help="Output machine-readable JSON.")
168
+ @_CONFIG_OPTION
169
+ def scorecard(
170
+ repo: str,
171
+ milestone: int,
172
+ verbose: bool,
173
+ output_json: bool,
174
+ config_path: Path,
175
+ ) -> None:
176
+ from coord import github_ops # noqa: PLC0415
177
+ from coord.scorecard import build_milestone_scorecard, scorecard_to_dict # noqa: PLC0415
178
+ from coord.state import list_audit_log # noqa: PLC0415
179
+ from coord.usage import fetch_usage_rows, pricing_dict_from_config # noqa: PLC0415
180
+
181
+ cfg = _load_config(config_path)
182
+ repo_entry = cfg.repo(repo)
183
+ if repo_entry is None:
184
+ click.echo(f"error: unknown repo {repo!r} (not in coordinator.yml)", err=True)
185
+ raise SystemExit(2)
186
+ slug = repo_entry.github
187
+
188
+ try:
189
+ ms = github_ops.get_milestone(slug, milestone)
190
+ except Exception as e: # noqa: BLE001 — surface a clean CLI error
191
+ click.echo(f"error: could not resolve milestone {milestone} on {slug}: {e}", err=True)
192
+ raise SystemExit(1) from e
193
+ ms_title = ms.get("title") or str(milestone)
194
+
195
+ try:
196
+ issues = github_ops.get_milestone_issues(slug, ms_title, state="all")
197
+ except Exception as e: # noqa: BLE001
198
+ click.echo(f"error: could not list issues for milestone {ms_title!r}: {e}", err=True)
199
+ raise SystemExit(1) from e
200
+
201
+ try:
202
+ assignment_rows = fetch_usage_rows()
203
+ except Exception as e: # noqa: BLE001 — board data is best-effort here;
204
+ # an unreachable daemon shouldn't block the label-only metrics.
205
+ click.echo(f"warning: could not fetch board assignment rows: {e}", err=True)
206
+ assignment_rows = []
207
+
208
+ # Best-effort durability cross-check (see coord.scorecard's docstring) —
209
+ # never fatal; a slow/unreachable audit read just means the cross-check
210
+ # is skipped, board `status="merged"` remains the primary signal. Bound
211
+ # by the milestone's own creation date and paginate back to that bound
212
+ # (see _AUDIT_LOG_* constants above) rather than trusting a single
213
+ # newest-first page to reach an old milestone.
214
+ audit_entries: list[dict] = []
215
+ since_ts = _parse_github_timestamp(ms.get("created_at"))
216
+ try:
217
+ cursor: str | None = None
218
+ for _page in range(_AUDIT_LOG_MAX_PAGES):
219
+ audit_result = list_audit_log(
220
+ repo=repo, category="merge", event_type="merged",
221
+ since=since_ts, limit=_AUDIT_LOG_PAGE_SIZE, cursor=cursor,
222
+ )
223
+ audit_entries.extend(audit_result.get("entries") or [])
224
+ cursor = audit_result.get("next_cursor")
225
+ if not audit_result.get("has_more") or not cursor:
226
+ break
227
+ else:
228
+ click.echo(
229
+ "warning: audit log merge cross-check stopped after "
230
+ f"{_AUDIT_LOG_MAX_PAGES * _AUDIT_LOG_PAGE_SIZE} entries "
231
+ "(more were available) — durability cross-check may be "
232
+ "incomplete for the oldest issues in this milestone",
233
+ err=True,
234
+ )
235
+ except Exception as e: # noqa: BLE001
236
+ click.echo(f"warning: could not fetch audit log: {e}", err=True)
237
+
238
+ card = build_milestone_scorecard(
239
+ milestone=milestone,
240
+ milestone_title=ms_title,
241
+ repo_name=repo,
242
+ issues=issues,
243
+ assignment_rows=assignment_rows,
244
+ audit_entries=audit_entries,
245
+ pricing=pricing_dict_from_config(cfg.pricing),
246
+ )
247
+
248
+ if output_json:
249
+ click.echo(_json.dumps(scorecard_to_dict(card), indent=2, default=str))
250
+ return
251
+
252
+ click.echo(format_scorecard(card, verbose=verbose))