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,469 @@
1
+ """Fleet-scope: are the four deploy lanes running the same release? (#1630)
2
+
3
+ The four lanes named in the issue: each agent's ``~/.coord-venv`` (N of
4
+ them), the ``coord-serve`` process on the daemon host, the operator's
5
+ ``~/.coord-cli-venv``, and the locally-built ``tui/`` binary. The CLI venv
6
+ lane exists *specifically* because it was found three releases stale on
7
+ 2026-07-29 — silently driving `coord` commands without fixes everyone
8
+ believed were live.
9
+
10
+ This module never probes the filesystem itself. Two different sources feed
11
+ it, and #1806 is precisely about not conflating them:
12
+
13
+ * ``coord-serve``'s own version is a genuinely daemon-host-local fact (it can
14
+ only be introspected from the process actually running it) — the daemon's
15
+ health-poll tick gathers that into ``HealthContext.fleet.daemon_host``
16
+ (``coord.serve_app``'s ``FleetHealthRefresher``).
17
+ * The CLI venv's version is *not* a daemon-host fact — it's whichever
18
+ machine's filesystem the operator actually put ``~/.coord-cli-venv`` on,
19
+ which is very often a different box from the one running ``coord-serve``.
20
+ That fact rides the same transport every agent's ``~/.coord-venv`` already
21
+ does: each machine's own ``/health`` poll, via the ``cli_venv`` machine-
22
+ scope check in :mod:`coord.health.checks.deploy_lane_facts`. See #1806.
23
+
24
+ #1834 added a fifth kind of lane, and it is the one that makes this check
25
+ worth running: ``<unit> spawns (<machine>)`` — the version each *running*
26
+ coord service would actually hand its subprocesses, resolved from the live
27
+ process's own PATH. Every lane above measures an install; on 2026-08-04 all
28
+ of them agreed on 0.4.105 while the daemon spawned 0.4.103, because the
29
+ defect was not in any install but in what ``shutil.which("coord")`` found
30
+ first. Skew is only ever visible as a *relationship between* lanes, which is
31
+ why it is checked here and not by any amount of per-lane staleness logic.
32
+
33
+ Both checks below are fail-soft toward UNKNOWN, never toward OK: a lane no
34
+ machine has data for (an agent that never reported, a CLI venv that was
35
+ never configured anywhere) must not read as "in sync" just because there was
36
+ nothing to disagree with.
37
+
38
+ A third fleet check, ``fleet_webapp_bundle``, aggregates ``webapp_bundle``
39
+ (#1834 lane 5, the phone-webapp bundle `coord web --dist` serves) the same
40
+ way ``fleet_tui_binary`` aggregates ``tui_binary`` — same source
41
+ (:mod:`coord.health.checks.deploy_lane_facts`), same shape, same fail-soft
42
+ convention. It is deliberately **not** folded into the version-skew map
43
+ below: ``deploy/coord-web-dist-build.timer`` publishes continuously off
44
+ ``origin/main``'s SHA, decoupled on purpose from the pip release cadence
45
+ every other lane in this module compares against, so a bundle's "version" is
46
+ never comparable to ``coord-serve``'s or an agent venv's. What it CAN
47
+ report — staleness against its own source tree, and disagreement between
48
+ machines about which bundle is live — it does, on its own terms, exactly
49
+ like ``tui_binary`` already does for the same reason.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from coord.health.models import CheckResult, HealthContext, Severity
55
+ from coord.health.registry import check
56
+
57
+
58
+ def _machine_check_values(ctx: HealthContext, check_id: str) -> dict[str, dict]:
59
+ """machine_name -> that machine's reported ``values`` for *check_id*.
60
+
61
+ Only includes machines whose result for this check exists and didn't
62
+ error — a machine that never runs/reports this check is simply absent
63
+ from the returned dict (the right shape for "optional lane" checks like
64
+ ``cli_venv``/``tui_binary``, where most machines legitimately have
65
+ nothing to say). ``_agent_lane_versions`` below wants the opposite shape
66
+ — every machine present, ``None`` standing in for "no data" — because
67
+ ``agent_venv`` is mandatory on every machine, so it doesn't use this
68
+ helper.
69
+ """
70
+ out: dict[str, dict] = {}
71
+ if ctx.fleet is None:
72
+ return out
73
+ for name, entry in ctx.fleet.machines.items():
74
+ checks = (entry or {}).get("checks") or {}
75
+ for r in checks.get("results", []) or []:
76
+ if r.get("check_id") == check_id and not r.get("error"):
77
+ out[name] = r.get("values") or {}
78
+ break
79
+ return out
80
+
81
+
82
+ def _agent_lane_versions(ctx: HealthContext) -> dict[str, str | None]:
83
+ """machine_name -> its reported ``agent_venv`` version, or ``None``.
84
+
85
+ ``None`` covers both "machine offline" and "machine online but its
86
+ ``agent_venv`` check didn't run/errored" — both are "no data", not
87
+ "matches everyone else". Unlike ``_machine_check_values``, every machine
88
+ in ``ctx.fleet.machines`` gets an entry — ``agent_venv`` is mandatory,
89
+ so a machine with nothing to say about it is a missing lane, not an
90
+ inapplicable one.
91
+ """
92
+ out: dict[str, str | None] = {}
93
+ if ctx.fleet is None:
94
+ return out
95
+ for name, entry in ctx.fleet.machines.items():
96
+ checks = (entry or {}).get("checks") or {}
97
+ version = None
98
+ for r in checks.get("results", []) or []:
99
+ if r.get("check_id") == "agent_venv" and not r.get("error"):
100
+ version = (r.get("values") or {}).get("version") or None
101
+ break
102
+ out[name] = version
103
+ return out
104
+
105
+
106
+ def _machine_check_rows(ctx: HealthContext, check_id: str) -> dict[str, list[dict]]:
107
+ """machine_name -> every non-errored result row this machine reported for
108
+ *check_id*.
109
+
110
+ :func:`_machine_check_values` stops at the first match, which is right for
111
+ the singleton checks it was written for. ``spawned_coord`` (#1834) reports
112
+ one row **per running unit**, so a first-match read would see only
113
+ ``coord-agent`` and structurally miss ``coord-serve`` — the one unit whose
114
+ spawned version was the whole 2026-08-04 incident.
115
+ """
116
+ out: dict[str, list[dict]] = {}
117
+ if ctx.fleet is None:
118
+ return out
119
+ for name, entry in ctx.fleet.machines.items():
120
+ checks = (entry or {}).get("checks") or {}
121
+ rows = [
122
+ r
123
+ for r in (checks.get("results") or [])
124
+ if r.get("check_id") == check_id and not r.get("error")
125
+ ]
126
+ if rows:
127
+ out[name] = rows
128
+ return out
129
+
130
+
131
+ def _spawned_lanes(ctx: HealthContext) -> dict[str, str]:
132
+ """``<unit> spawns (<machine>)`` -> the version that unit would actually
133
+ spawn, for every running coord service across the fleet (#1834).
134
+
135
+ This is the lane that did not exist on 2026-08-04, and its absence is why
136
+ every other lane in this check read green while the fleet was running two
137
+ versions: each of them measures an *install*, and the defect lived in what
138
+ ``shutil.which("coord")`` resolved to inside a live service's PATH.
139
+ See :mod:`coord.health.checks.spawned_coord`.
140
+
141
+ Only units with a resolvable spawned version become lanes. A unit whose
142
+ PATH has no ``coord`` on it at all is deliberately **not** a lane: its
143
+ subprocesses fall back to ``python -m coord.cli`` on the parent's own
144
+ interpreter, which cannot disagree with the parent, so admitting it as a
145
+ null lane would manufacture a permanent "missing lane" UNKNOWN on every
146
+ correctly-deployed fleet.
147
+ """
148
+ out: dict[str, str] = {}
149
+ for machine, rows in _machine_check_rows(ctx, "spawned_coord").items():
150
+ for row in rows:
151
+ values = row.get("values") or {}
152
+ version = values.get("version")
153
+ unit = row.get("subject") or values.get("unit")
154
+ if version and unit:
155
+ out[f"{unit} spawns ({machine})"] = version
156
+ return out
157
+
158
+
159
+ def _cli_venv_lanes(ctx: HealthContext) -> dict[str, str | None]:
160
+ """``~/.coord-cli-venv (<machine>)`` -> version, for every machine whose
161
+ own ``cli_venv`` check (#1806) reports one present.
162
+
163
+ Named per-machine, unlike the daemon-host fact it replaces, because more
164
+ than one machine can plausibly have a CLI venv (more than one operator).
165
+ When *no* machine reports one present, a single ``"~/.coord-cli-venv":
166
+ None`` entry stands in — "no data anywhere", not "matches everyone else"
167
+ just because nothing disagreed.
168
+ """
169
+ out: dict[str, str | None] = {}
170
+ for name, values in _machine_check_values(ctx, "cli_venv").items():
171
+ if values.get("present"):
172
+ out[f"~/.coord-cli-venv ({name})"] = values.get("version") or None
173
+ if not out:
174
+ out["~/.coord-cli-venv"] = None
175
+ return out
176
+
177
+
178
+ @check(
179
+ id="fleet_deploy_lanes",
180
+ scope="fleet",
181
+ title="deploy lanes",
182
+ order=10,
183
+ description=(
184
+ "Every ~/.coord-venv (per agent), the daemon's own coord-serve "
185
+ "install, ~/.coord-cli-venv, and the coord each running service "
186
+ "would actually spawn all report the same coordinator version."
187
+ ),
188
+ )
189
+ def probe_deploy_lanes(ctx: HealthContext) -> CheckResult:
190
+ if ctx.fleet is None:
191
+ return CheckResult(
192
+ check_id="fleet_deploy_lanes",
193
+ scope="fleet",
194
+ severity=Severity.UNKNOWN,
195
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
196
+ )
197
+
198
+ lanes: dict[str, str | None] = dict(_agent_lane_versions(ctx))
199
+ dh = ctx.fleet.daemon_host or {}
200
+ lanes["coord-serve (daemon host)"] = dh.get("coord_serve_version")
201
+ lanes.update(_cli_venv_lanes(ctx))
202
+ # #1834: what each running service would actually SPAWN. Added last and
203
+ # only where known, so it can introduce skew but never a missing lane.
204
+ lanes.update(_spawned_lanes(ctx))
205
+
206
+ known = {v for v in lanes.values() if v}
207
+ missing = sorted(name for name, v in lanes.items() if not v)
208
+
209
+ if not known:
210
+ return CheckResult(
211
+ check_id="fleet_deploy_lanes",
212
+ scope="fleet",
213
+ severity=Severity.UNKNOWN,
214
+ headroom="no lane has a resolvable version yet",
215
+ detail=f"no data for: {', '.join(missing)}" if missing else "",
216
+ values={"lanes": lanes},
217
+ )
218
+
219
+ if len(known) > 1:
220
+ by_version: dict[str, list[str]] = {}
221
+ for name, v in lanes.items():
222
+ if v:
223
+ by_version.setdefault(v, []).append(name)
224
+ skew_desc = "; ".join(
225
+ f"{v}: {', '.join(sorted(names))}" for v, names in sorted(by_version.items())
226
+ )
227
+ return CheckResult(
228
+ check_id="fleet_deploy_lanes",
229
+ scope="fleet",
230
+ severity=Severity.CRIT,
231
+ headroom=f"{len(known)} versions live across the fleet",
232
+ detail=skew_desc,
233
+ threshold="crit when any lane disagrees",
234
+ values={"lanes": lanes},
235
+ )
236
+
237
+ (version,) = known
238
+ headroom = f"all lanes on {version}"
239
+ if missing:
240
+ headroom += f" ({len(missing)} lane(s) with no data)"
241
+ return CheckResult(
242
+ check_id="fleet_deploy_lanes",
243
+ scope="fleet",
244
+ severity=Severity.OK if not missing else Severity.UNKNOWN,
245
+ headroom=headroom,
246
+ detail=f"no data for: {', '.join(missing)}" if missing else "",
247
+ values={"lanes": lanes},
248
+ )
249
+
250
+
251
+ @check(
252
+ id="fleet_tui_binary",
253
+ scope="fleet",
254
+ title="tui binary",
255
+ order=11,
256
+ description=(
257
+ "The locally-built tui/ binary is not older than the tui/ source "
258
+ "tree it was supposedly built from, on every machine that has one."
259
+ ),
260
+ )
261
+ def probe_tui_binary(ctx: HealthContext) -> CheckResult:
262
+ """Aggregates every machine's own ``tui_binary`` machine-scope check
263
+ (:mod:`coord.health.checks.deploy_lane_facts`, #1806) instead of
264
+ ``os.stat``-ing a single path on the daemon host — the daemon host is
265
+ frequently not the operator's machine, so a single-path check there was
266
+ structurally blind to the binary that actually matters.
267
+ """
268
+ if ctx.fleet is None:
269
+ return CheckResult(
270
+ check_id="fleet_tui_binary",
271
+ scope="fleet",
272
+ severity=Severity.UNKNOWN,
273
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
274
+ )
275
+
276
+ facts = _machine_check_values(ctx, "tui_binary")
277
+ present = {name: v for name, v in facts.items() if v.get("present")}
278
+
279
+ if not present:
280
+ return CheckResult(
281
+ check_id="fleet_tui_binary",
282
+ scope="fleet",
283
+ severity=Severity.UNKNOWN,
284
+ headroom="no machine reports a coord-tui binary",
285
+ detail=(
286
+ "build and install it (`cd tui && cargo build && cp "
287
+ "target/debug/coord-tui ~/.local/bin/coord-tui`) on the "
288
+ "machine you actually run the tui from, or set "
289
+ "health.tui_binary_path if it lives elsewhere"
290
+ ),
291
+ values={"machines": facts},
292
+ )
293
+
294
+ # Compare each machine against ITS OWN source tree — a machine with no
295
+ # tui/ checkout has nothing to compare against and is left out of the
296
+ # verdict (present, uncomparable), same as the single-machine case
297
+ # always was; it just no longer speaks for the whole fleet.
298
+ stale: list[tuple[str, float, float]] = []
299
+ comparable: list[str] = []
300
+ for name, values in present.items():
301
+ binary_mtime = values.get("binary_mtime")
302
+ source_mtime = values.get("source_mtime")
303
+ if binary_mtime is None or source_mtime is None:
304
+ continue
305
+ comparable.append(name)
306
+ if source_mtime > binary_mtime:
307
+ stale.append((name, binary_mtime, source_mtime))
308
+
309
+ if stale:
310
+ # Worst (most hours stale) first, but name every stale machine —
311
+ # this check exists specifically so a stale lane is never silent.
312
+ stale.sort(key=lambda t: (t[2] - t[1]), reverse=True)
313
+ names = ", ".join(n for n, _, _ in stale)
314
+ _worst_name, worst_bm, worst_sm = stale[0]
315
+ stale_hours = (worst_sm - worst_bm) / 3600.0
316
+ return CheckResult(
317
+ check_id="fleet_tui_binary",
318
+ scope="fleet",
319
+ severity=Severity.WARN,
320
+ headroom=f"{names}: binary is {stale_hours:.1f}h older than tui/ source",
321
+ detail=f"rebuild the tui/ binary on: {names}",
322
+ threshold="warn when any machine's source/ is newer than its built binary",
323
+ values={"machines": facts, "stale": [n for n, _, _ in stale]},
324
+ )
325
+
326
+ if not comparable:
327
+ return CheckResult(
328
+ check_id="fleet_tui_binary",
329
+ scope="fleet",
330
+ severity=Severity.OK,
331
+ headroom=(
332
+ f"binary present on {', '.join(sorted(present))} "
333
+ "(no source tree found to compare)"
334
+ ),
335
+ values={"machines": facts},
336
+ )
337
+
338
+ return CheckResult(
339
+ check_id="fleet_tui_binary",
340
+ scope="fleet",
341
+ severity=Severity.OK,
342
+ headroom=f"up to date with tui/ source on {', '.join(sorted(comparable))}",
343
+ values={"machines": facts},
344
+ )
345
+
346
+
347
+ @check(
348
+ id="fleet_webapp_bundle",
349
+ scope="fleet",
350
+ title="webapp bundle",
351
+ order=13,
352
+ description=(
353
+ "The dist/ bundle `coord web --dist` serves is not older than the "
354
+ "coord/dashboard/webapp/ source tree it was supposedly built from, "
355
+ "on every machine that has one (#1834 lane 5)."
356
+ ),
357
+ )
358
+ def probe_webapp_bundle(ctx: HealthContext) -> CheckResult:
359
+ """Aggregates every machine's own ``webapp_bundle`` machine-scope check
360
+ — the same "measure locally, judge centrally" split :func:`probe_tui_binary`
361
+ uses above, for the same reason: the machine actually running `coord web`
362
+ is very often not the daemon host.
363
+
364
+ Deliberately NOT compared against the released wheel's version, unlike
365
+ every other lane in this module: `coord-web-dist-build.timer` publishes
366
+ continuously off `origin/main`'s SHA (#1543), decoupled on purpose from
367
+ the ~/.coord-venv release cadence, so "is it at the released version?" is
368
+ not a well-formed question for it — see docs/AGENT_OPERATIONS.md's
369
+ `coord release verify` section. What IS well-formed, and what this
370
+ checks: whether the live bundle is stale relative to the source it
371
+ claims to have been built from, and whether machines that both run
372
+ `coord web` agree on which bundle that is.
373
+ """
374
+ if ctx.fleet is None:
375
+ return CheckResult(
376
+ check_id="fleet_webapp_bundle",
377
+ scope="fleet",
378
+ severity=Severity.UNKNOWN,
379
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
380
+ )
381
+
382
+ facts = _machine_check_values(ctx, "webapp_bundle")
383
+ present = {name: v for name, v in facts.items() if v.get("present")}
384
+
385
+ if not present:
386
+ return CheckResult(
387
+ check_id="fleet_webapp_bundle",
388
+ scope="fleet",
389
+ severity=Severity.UNKNOWN,
390
+ headroom="no machine reports a coord-web-dist bundle",
391
+ detail=(
392
+ "install deploy/coord-web-dist-build.service + .timer on the "
393
+ "machine that runs `coord web` (see docs/AGENT_OPERATIONS.md), "
394
+ "or set health.webapp_dist_path if it lives elsewhere"
395
+ ),
396
+ values={"machines": facts},
397
+ )
398
+
399
+ # Compare each machine against ITS OWN source tree, same reasoning as
400
+ # fleet_tui_binary: a machine with no webapp/ checkout has nothing to
401
+ # compare against and is left out of the verdict (present, uncomparable).
402
+ stale: list[tuple[str, float, float]] = []
403
+ comparable: list[str] = []
404
+ for name, values in present.items():
405
+ dist_mtime = values.get("dist_mtime")
406
+ source_mtime = values.get("source_mtime")
407
+ if dist_mtime is None or source_mtime is None:
408
+ continue
409
+ comparable.append(name)
410
+ if source_mtime > dist_mtime:
411
+ stale.append((name, dist_mtime, source_mtime))
412
+
413
+ if stale:
414
+ stale.sort(key=lambda t: (t[2] - t[1]), reverse=True)
415
+ names = ", ".join(n for n, _, _ in stale)
416
+ _worst_name, worst_dm, worst_sm = stale[0]
417
+ stale_hours = (worst_sm - worst_dm) / 3600.0
418
+ return CheckResult(
419
+ check_id="fleet_webapp_bundle",
420
+ scope="fleet",
421
+ severity=Severity.WARN,
422
+ headroom=f"{names}: bundle is {stale_hours:.1f}h older than webapp/ source",
423
+ detail=f"check coord-web-dist-build.timer on: {names}",
424
+ threshold="warn when any machine's webapp/ source is newer than its live bundle",
425
+ values={"machines": facts, "stale": [n for n, _, _ in stale]},
426
+ )
427
+
428
+ # Two machines both running `coord web` but serving different builds is
429
+ # its own drift, independent of staleness against source — most fleets
430
+ # only ever have one, but when there are two they must agree.
431
+ shas = {v.get("sha") for v in present.values() if v.get("sha")}
432
+ if len(shas) > 1:
433
+ by_sha: dict[str, list[str]] = {}
434
+ for name, v in present.items():
435
+ sha = v.get("sha")
436
+ if sha:
437
+ by_sha.setdefault(sha, []).append(name)
438
+ skew_desc = "; ".join(
439
+ f"{sha}: {', '.join(sorted(names))}" for sha, names in sorted(by_sha.items())
440
+ )
441
+ return CheckResult(
442
+ check_id="fleet_webapp_bundle",
443
+ scope="fleet",
444
+ severity=Severity.WARN,
445
+ headroom=f"{len(shas)} different bundles live across the fleet",
446
+ detail=skew_desc,
447
+ threshold="warn when machines serving coord web disagree on the live bundle",
448
+ values={"machines": facts},
449
+ )
450
+
451
+ if not comparable:
452
+ return CheckResult(
453
+ check_id="fleet_webapp_bundle",
454
+ scope="fleet",
455
+ severity=Severity.OK,
456
+ headroom=(
457
+ f"bundle present on {', '.join(sorted(present))} "
458
+ "(no source tree found to compare)"
459
+ ),
460
+ values={"machines": facts},
461
+ )
462
+
463
+ return CheckResult(
464
+ check_id="fleet_webapp_bundle",
465
+ scope="fleet",
466
+ severity=Severity.OK,
467
+ headroom=f"up to date with webapp/ source on {', '.join(sorted(comparable))}",
468
+ values={"machines": facts},
469
+ )
@@ -0,0 +1,69 @@
1
+ """Fleet-scope: phantom ``running`` board rows (#1630).
2
+
3
+ A phantom ``running`` row (board says running, the owning agent no longer
4
+ agrees) silently disables ``coord retry`` fleet-wide — see
5
+ ``coord.reconcile.describe_no_candidate_machines`` and ``coord/diagnose.py``
6
+ for the interactive/manual side of this failure mode. This check is the
7
+ passive, always-on fleet-health signal for the same class of row: the
8
+ daemon's health-poll tick cross-references board ``running`` rows against
9
+ each machine's own ``/status`` and hands this probe the count it found — read
10
+ only, never a write, never a finalize (that stays ``coord diagnose``'s job).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from coord.health.models import CheckResult, HealthContext, Severity
16
+ from coord.health.registry import check
17
+
18
+
19
+ @check(
20
+ id="fleet_phantom_running",
21
+ scope="fleet",
22
+ title="phantom running rows",
23
+ order=21,
24
+ description=(
25
+ "Board rows marked running whose owning agent no longer reports "
26
+ "them as running — known to silently disable `coord retry`."
27
+ ),
28
+ )
29
+ def probe_phantom_running(ctx: HealthContext) -> CheckResult:
30
+ if ctx.fleet is None:
31
+ return CheckResult(
32
+ check_id="fleet_phantom_running",
33
+ scope="fleet",
34
+ severity=Severity.UNKNOWN,
35
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
36
+ )
37
+
38
+ dh = ctx.fleet.daemon_host or {}
39
+ phantom = dh.get("phantom_running")
40
+ if phantom is None:
41
+ return CheckResult(
42
+ check_id="fleet_phantom_running",
43
+ scope="fleet",
44
+ severity=Severity.UNKNOWN,
45
+ headroom="no phantom-row scan yet",
46
+ )
47
+
48
+ count = len(phantom)
49
+ if count == 0:
50
+ return CheckResult(
51
+ check_id="fleet_phantom_running",
52
+ scope="fleet",
53
+ severity=Severity.OK,
54
+ headroom="0 phantom running rows",
55
+ )
56
+
57
+ sample = ", ".join(
58
+ f"{p.get('repo_name')}#{p.get('issue_number')}@{p.get('machine')}"
59
+ for p in phantom[:5]
60
+ )
61
+ return CheckResult(
62
+ check_id="fleet_phantom_running",
63
+ scope="fleet",
64
+ severity=Severity.CRIT,
65
+ headroom=f"{count} phantom running row{'s' if count != 1 else ''}",
66
+ detail=f"e.g. {sample}" + (", ..." if count > 5 else ""),
67
+ threshold="crit when any phantom row is found",
68
+ values={"count": count, "assignment_ids": [p.get("assignment_id") for p in phantom]},
69
+ )
@@ -0,0 +1,151 @@
1
+ """Fleet-scope: does any machine's installed unit drift from the released
2
+ unit files? (#1831, #1927)
3
+
4
+ Aggregates every machine's own `unit_drift` machine-scope check
5
+ (:mod:`coord.health.checks.unit_drift`) instead of reading the filesystem
6
+ itself — a unit file is inherently a fact about whichever machine it's
7
+ installed on, exactly like `cli_venv`/`tui_binary`
8
+ (:mod:`coord.health.checks.deploy_lane_facts`) and the fleet aggregator that
9
+ reads them (:mod:`coord.health.checks.fleet_deploy_lanes`).
10
+
11
+ Fail-soft toward UNKNOWN, never toward OK: a fleet with no machine yet
12
+ reporting `unit_drift` (every agent still predates #1831) must not read as
13
+ "nothing wrong" just because nothing has disagreed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from coord.health.models import CheckResult, HealthContext, Severity
19
+ from coord.health.registry import check
20
+
21
+
22
+ def _all_unit_drift_results(ctx: HealthContext) -> list[tuple[str, dict]]:
23
+ """[(machine_name, result_dict), ...] for every reported `unit_drift`
24
+ result across the fleet — one entry per (machine, unit), since the
25
+ machine-scope check returns one result per deploy-lane unit."""
26
+ out: list[tuple[str, dict]] = []
27
+ if ctx.fleet is None:
28
+ return out
29
+ for name, entry in ctx.fleet.machines.items():
30
+ checks = (entry or {}).get("checks") or {}
31
+ for r in checks.get("results", []) or []:
32
+ if r.get("check_id") == "unit_drift" and not r.get("error"):
33
+ out.append((name, r))
34
+ return out
35
+
36
+
37
+ @check(
38
+ id="fleet_unit_drift",
39
+ scope="fleet",
40
+ title="unit drift",
41
+ order=12,
42
+ description=(
43
+ "Every machine's installed systemd units match deploy/, and none "
44
+ "shadow the release with an editable checkout on PATH (#1831)."
45
+ ),
46
+ )
47
+ def probe_fleet_unit_drift(ctx: HealthContext) -> CheckResult:
48
+ if ctx.fleet is None:
49
+ return CheckResult(
50
+ check_id="fleet_unit_drift",
51
+ scope="fleet",
52
+ severity=Severity.UNKNOWN,
53
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
54
+ )
55
+
56
+ all_results = _all_unit_drift_results(ctx)
57
+ if not all_results:
58
+ return CheckResult(
59
+ check_id="fleet_unit_drift",
60
+ scope="fleet",
61
+ severity=Severity.UNKNOWN,
62
+ headroom="no machine has reported unit_drift data yet",
63
+ detail="every agent predates #1831, or unit_drift is disabled",
64
+ )
65
+
66
+ shadowed = [
67
+ (m, r) for m, r in all_results if r.get("severity") == "crit"
68
+ ]
69
+ stale = [(m, r) for m, r in all_results if r.get("severity") == "warn"]
70
+ checked = [(m, r) for m, r in all_results if (r.get("values") or {}).get("installed")]
71
+ # #1927: a machine that diffed against its own git working copy proved
72
+ # nothing — a stale checkout and a stale installed unit agree with each
73
+ # other. Those results arrive as UNKNOWN and must not be counted toward
74
+ # the fleet's green.
75
+ unverified = [
76
+ (m, r)
77
+ for m, r in all_results
78
+ if (r.get("values") or {}).get("installed")
79
+ and (r.get("values") or {}).get("reference_verified") is False
80
+ ]
81
+
82
+ values = {
83
+ "machines": sorted({m for m, _ in all_results}),
84
+ "checked_units": len(checked),
85
+ "shadowed": [
86
+ {"machine": m, "unit": r.get("subject")} for m, r in shadowed
87
+ ],
88
+ "stale": [{"machine": m, "unit": r.get("subject")} for m, r in stale],
89
+ "unverified_reference": [
90
+ {"machine": m, "unit": r.get("subject")} for m, r in unverified
91
+ ],
92
+ }
93
+
94
+ if shadowed:
95
+ names = ", ".join(f"{m}/{r.get('subject')}" for m, r in shadowed)
96
+ return CheckResult(
97
+ check_id="fleet_unit_drift",
98
+ scope="fleet",
99
+ severity=Severity.CRIT,
100
+ headroom=f"editable checkout shadows the release on PATH: {names}",
101
+ detail=(
102
+ "shutil.which(\"coord\") in a unit's subprocesses resolves a "
103
+ "stale checkout instead of the pinned release — see each "
104
+ "machine's own unit_drift check for the fix"
105
+ ),
106
+ threshold="crit when any unit's PATH lets a .venv/bin entry precede the release",
107
+ values=values,
108
+ )
109
+
110
+ if stale:
111
+ names = ", ".join(f"{m}/{r.get('subject')}" for m, r in stale)
112
+ return CheckResult(
113
+ check_id="fleet_unit_drift",
114
+ scope="fleet",
115
+ severity=Severity.WARN,
116
+ headroom=f"installed unit(s) stale vs the released units: {names}",
117
+ detail="cp the reference unit over the installed one and restart it — see the machine-scope unit_drift detail for the exact command",
118
+ threshold="warn when any installed unit's content != the released unit",
119
+ values=values,
120
+ )
121
+
122
+ if unverified:
123
+ names = ", ".join(
124
+ sorted({f"{m}/{r.get('subject')}" for m, r in unverified})
125
+ )
126
+ return CheckResult(
127
+ check_id="fleet_unit_drift",
128
+ scope="fleet",
129
+ severity=Severity.UNKNOWN,
130
+ headroom=f"reference is an unverified working copy on: {names}",
131
+ detail=(
132
+ "these machines diffed their installed units against a git "
133
+ "checkout nothing verifies is current, so a match proves "
134
+ "nothing — both sides go stale together (#1927). Install a "
135
+ "release wheel there; it ships the reference units."
136
+ ),
137
+ values=values,
138
+ )
139
+
140
+ return CheckResult(
141
+ check_id="fleet_unit_drift",
142
+ scope="fleet",
143
+ severity=Severity.OK,
144
+ headroom=(
145
+ f"{len(checked)} installed unit(s) across {len(values['machines'])} "
146
+ "machine(s) match the released units"
147
+ if checked
148
+ else "no machine has any deploy-lane unit installed"
149
+ ),
150
+ values=values,
151
+ )