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,458 @@
1
+ """Per-machine facts for two of the fleet deploy-lane checks (#1806).
2
+
3
+ Root cause of #1806: `fleet_deploy_lanes`'s ``~/.coord-cli-venv`` lane and
4
+ the whole of `fleet_tui_binary` used to be gathered by ``os.stat``-ing the
5
+ **daemon host's** filesystem
6
+ (``coord.health.fleet_snapshot.FleetHealthRefresher._daemon_host_facts``).
7
+ That is correct for a fact that can only exist on the daemon process itself
8
+ (``coord-serve``'s own venv) — it is wrong for these two, whose subject is a
9
+ path on the **operator's** machine, which is very often a different box
10
+ from the one running ``coord-serve``. When the two differ, the daemon was
11
+ reporting on its own, usually-absent, copy of paths that live somewhere
12
+ else entirely — and reporting the *opposite* of the truth (#1806's live
13
+ example: WARN "stale" while the operator's real binary was current;
14
+ UNKNOWN "no data" while the operator's real CLI venv was on the release
15
+ everyone believed was live).
16
+
17
+ These two **machine-scope** checks report the same two raw facts (CLI-venv
18
+ version; tui-binary mtime vs. newest ``tui/`` source mtime) from wherever
19
+ they actually run — every agent's own ``/health`` poll, the transport #1630
20
+ already built for exactly this "measure locally, judge centrally" split.
21
+ `coord.health.checks.fleet_deploy_lanes` aggregates these across every
22
+ ``ctx.fleet.machines`` entry instead of trusting a single host's local
23
+ ``os.stat`` to answer a fleet-wide question.
24
+
25
+ Absence is the *common* case here — most machines in a fleet have neither
26
+ an operator's CLI venv nor a local ``tui/`` checkout+build — so both probes
27
+ report that plainly as ``OK`` ("not present on this machine") rather than
28
+ ``UNKNOWN``/``WARN``: a box that was never meant to have either lane must
29
+ not read as faulty just because it doesn't.
30
+
31
+ A third check, ``webapp_bundle``, joins these two for the same reason and
32
+ the same shape (#1834 lane 5, the phone-webapp bundle `coord web --dist`
33
+ serves): the machine actually running `coord web` is very often not the
34
+ daemon host either. It is deliberately **not** graded against the released
35
+ wheel's version the way ``agent_venv``/``spawned_coord`` are —
36
+ ``deploy/coord-web-dist-build.timer`` publishes continuously off
37
+ ``origin/main``'s SHA (#1543), decoupled on purpose from the
38
+ ``~/.coord-venv`` release cadence, so "is it on the released version?" is
39
+ not a well-formed question for it. What *is* well-formed, and what this
40
+ checks — mirroring ``tui_binary`` exactly — is whether the live bundle is
41
+ older than the ``coord/dashboard/webapp/`` source tree it claims to have
42
+ been built from.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import subprocess
48
+
49
+ from coord.dist_name import CANDIDATE_NAMES
50
+ from coord.health.checks.agent_install import pip_show
51
+ from coord.health.models import CheckResult, HealthContext, Severity
52
+ from coord.health.registry import check
53
+ from coord.health.units import expand, shorten_path
54
+
55
+ # Same documented default locations `coord.health.fleet_snapshot` used to
56
+ # stat on the daemon host — now stat'd wherever this check actually runs.
57
+ # `health.cli_venv_python` / `health.tui_binary_path` / `health.tui_source_dir`
58
+ # override these, same convention as `agent_venv_python`: None means "use the
59
+ # documented default location", NOT "disable the lane".
60
+ _DEFAULT_CLI_VENV_PYTHON = "~/.coord-cli-venv/bin/python3"
61
+ _DEFAULT_TUI_BINARY = "~/.local/bin/coord-tui"
62
+ # Mirrors `deploy/coord-web-dist-build.sh`'s `$LIVE_LINK` — the symlink
63
+ # `coord-web-dist-build.timer` atomically repoints at each new release.
64
+ _DEFAULT_WEBAPP_DIST = "~/coord-web-dist"
65
+
66
+ # Cap on the tui/ source walk, mirroring the old daemon-side walk: the tree
67
+ # is a few hundred .rs files, so anything past this is a misconfigured
68
+ # `tui_source_dir` pointed at something huge, and a machine's own /health
69
+ # tick must not spend its budget discovering that. A partial walk still
70
+ # yields a *lower bound* on the newest mtime — it can only under-report
71
+ # staleness, never fabricate it.
72
+ _MAX_TUI_SOURCE_FILES = 5000
73
+ # Same cap, same reasoning, for the webapp/ source walk below.
74
+ _MAX_WEBAPP_SOURCE_FILES = 5000
75
+ # Directories a webapp source walk must never descend into: dependency and
76
+ # build output trees that dwarf the actual source and would either blow the
77
+ # file cap on noise or (worse) let a stale `dist/` sitting inside a checkout
78
+ # masquerade as "source newer than itself".
79
+ _WEBAPP_SOURCE_SKIP_DIRS = {"node_modules", "dist", "build", "coverage"}
80
+
81
+
82
+ def resolve_cli_venv_python(ctx: HealthContext):
83
+ """The interpreter for this machine's ``~/.coord-cli-venv``, if any."""
84
+ configured = getattr(ctx.thresholds, "cli_venv_python", None)
85
+ if configured:
86
+ return expand(configured, ctx.home)
87
+ return expand(_DEFAULT_CLI_VENV_PYTHON, ctx.home)
88
+
89
+
90
+ def resolve_tui_binary_path(ctx: HealthContext):
91
+ """This machine's locally-built ``coord-tui`` binary, if any."""
92
+ configured = getattr(ctx.thresholds, "tui_binary_path", None)
93
+ if configured:
94
+ return expand(configured, ctx.home)
95
+ return expand(_DEFAULT_TUI_BINARY, ctx.home)
96
+
97
+
98
+ def resolve_tui_source_dir(ctx: HealthContext):
99
+ """``<checkout>/tui/src`` for the first local checkout that has one.
100
+
101
+ Derived from ``ctx.checkouts`` (the same locally-existing checkouts
102
+ every other checkout-scope probe sees) rather than guessed relative to
103
+ the binary path — a `target/release/...` install path says nothing
104
+ reliable about where the sources live, and comparing against the
105
+ *wrong* tree would manufacture staleness rather than report it.
106
+ Configured `health.tui_source_dir` wins outright when set.
107
+ """
108
+ configured = getattr(ctx.thresholds, "tui_source_dir", None)
109
+ if configured:
110
+ return expand(configured, ctx.home)
111
+ for checkout in ctx.checkouts:
112
+ candidate = checkout.path / "tui" / "src"
113
+ if candidate.is_dir():
114
+ return candidate
115
+ return None
116
+
117
+
118
+ def resolve_webapp_dist_path(ctx: HealthContext):
119
+ """This machine's live ``coord web --dist`` bundle, if any (#1834)."""
120
+ configured = getattr(ctx.thresholds, "webapp_dist_path", None)
121
+ if configured:
122
+ return expand(configured, ctx.home)
123
+ return expand(_DEFAULT_WEBAPP_DIST, ctx.home)
124
+
125
+
126
+ def resolve_webapp_source_dir(ctx: HealthContext):
127
+ """``<checkout>/coord/dashboard/webapp/src`` for the first local checkout
128
+ that has one. Same convention as :func:`resolve_tui_source_dir`."""
129
+ configured = getattr(ctx.thresholds, "webapp_source_dir", None)
130
+ if configured:
131
+ return expand(configured, ctx.home)
132
+ for checkout in ctx.checkouts:
133
+ candidate = checkout.path / "coord" / "dashboard" / "webapp" / "src"
134
+ if candidate.is_dir():
135
+ return candidate
136
+ return None
137
+
138
+
139
+ def _newest_webapp_source_mtime(source_dir):
140
+ """Newest mtime under *source_dir*, skipping dependency/build dirs.
141
+
142
+ Unlike :func:`_newest_rust_source_mtime` this is deliberately NOT
143
+ extension-filtered: a webapp source tree mixes ``.ts``/``.tsx``/``.css``/
144
+ ``.html`` meaningfully and filtering would only risk under-reporting
145
+ staleness. ``node_modules``/``dist``/``build`` are the trees that
146
+ actually need skipping (size, and — for ``dist``/``build`` sitting
147
+ inside a checkout — self-reference), so those are excluded outright.
148
+ """
149
+ newest = 0.0
150
+ seen = 0
151
+ try:
152
+ if not source_dir.is_dir():
153
+ return None
154
+ stack = [source_dir]
155
+ while stack:
156
+ current = stack.pop()
157
+ try:
158
+ entries = list(current.iterdir())
159
+ except OSError:
160
+ continue
161
+ for entry in entries:
162
+ name = entry.name
163
+ if name.startswith(".") or name in _WEBAPP_SOURCE_SKIP_DIRS:
164
+ continue
165
+ try:
166
+ if entry.is_dir():
167
+ stack.append(entry)
168
+ continue
169
+ newest = max(newest, entry.stat().st_mtime)
170
+ except OSError:
171
+ continue
172
+ seen += 1
173
+ if seen >= _MAX_WEBAPP_SOURCE_FILES:
174
+ stack.clear()
175
+ break
176
+ except OSError:
177
+ return newest or None
178
+ return newest or None
179
+
180
+
181
+ def _newest_rust_source_mtime(source_dir):
182
+ """Newest ``*.rs`` mtime under *source_dir*, or None if there are none.
183
+
184
+ Skips ``target``/hidden directories so a ``tui_source_dir`` pointed at a
185
+ crate root (rather than its ``src/``) degrades to slow-but-correct
186
+ instead of walking a multi-GB build tree.
187
+ """
188
+ newest = 0.0
189
+ seen = 0
190
+ try:
191
+ if not source_dir.is_dir():
192
+ return None
193
+ stack = [source_dir]
194
+ while stack:
195
+ current = stack.pop()
196
+ try:
197
+ entries = list(current.iterdir())
198
+ except OSError:
199
+ continue
200
+ for entry in entries:
201
+ name = entry.name
202
+ if name.startswith(".") or name == "target":
203
+ continue
204
+ try:
205
+ if entry.is_dir():
206
+ stack.append(entry)
207
+ continue
208
+ if not name.endswith(".rs"):
209
+ continue
210
+ newest = max(newest, entry.stat().st_mtime)
211
+ except OSError:
212
+ continue
213
+ seen += 1
214
+ if seen >= _MAX_TUI_SOURCE_FILES:
215
+ stack.clear()
216
+ break
217
+ except OSError:
218
+ return newest or None
219
+ return newest or None
220
+
221
+
222
+ @check(
223
+ id="cli_venv",
224
+ scope="machine",
225
+ title="cli venv",
226
+ order=42,
227
+ description=(
228
+ "This machine's ~/.coord-cli-venv coordinator version, when "
229
+ "it has one — the operator's CLI venv lane of fleet_deploy_lanes (#1806)."
230
+ ),
231
+ )
232
+ def probe_cli_venv(ctx: HealthContext) -> CheckResult:
233
+ python = resolve_cli_venv_python(ctx)
234
+ if not python.exists():
235
+ # The overwhelming common case: most machines are workers, not the
236
+ # operator's own box, and never had this venv created. That is not
237
+ # a fault — fleet_deploy_lanes reads "no machine reports this lane"
238
+ # as UNKNOWN, not this machine's absence as one.
239
+ return CheckResult(
240
+ check_id="cli_venv",
241
+ scope="machine",
242
+ severity=Severity.OK,
243
+ headroom="not present on this machine",
244
+ values={"python": str(python), "present": False, "version": None},
245
+ )
246
+
247
+ try:
248
+ fields = pip_show(python)
249
+ except (OSError, subprocess.SubprocessError) as exc:
250
+ return CheckResult(
251
+ check_id="cli_venv",
252
+ scope="machine",
253
+ severity=Severity.UNKNOWN,
254
+ headroom=f"could not run pip show ({type(exc).__name__})",
255
+ error=str(exc),
256
+ values={"python": str(python), "present": True, "version": None},
257
+ )
258
+
259
+ version = fields.get("Version") or None
260
+ if not version:
261
+ tried = " or ".join(CANDIDATE_NAMES)
262
+ return CheckResult(
263
+ check_id="cli_venv",
264
+ scope="machine",
265
+ severity=Severity.UNKNOWN,
266
+ headroom=f"neither {tried} installed in {shorten_path(str(python), str(ctx.home))}",
267
+ error="pip show returned nothing",
268
+ values={"python": str(python), "present": True, "version": None},
269
+ )
270
+
271
+ return CheckResult(
272
+ check_id="cli_venv",
273
+ scope="machine",
274
+ severity=Severity.OK,
275
+ headroom=f"pypi {version}",
276
+ values={
277
+ "python": str(python),
278
+ "present": True,
279
+ "version": version,
280
+ "editable": bool(fields.get("Editable project location")),
281
+ },
282
+ )
283
+
284
+
285
+ @check(
286
+ id="tui_binary",
287
+ scope="machine",
288
+ title="tui binary",
289
+ order=43,
290
+ description=(
291
+ "This machine's locally-built tui/ binary vs. the tui/ source tree "
292
+ "it was built from — feeds the fleet_tui_binary check (#1806)."
293
+ ),
294
+ )
295
+ def probe_tui_binary(ctx: HealthContext) -> CheckResult:
296
+ binary_path = resolve_tui_binary_path(ctx)
297
+ values: dict = {"path": str(binary_path)}
298
+
299
+ try:
300
+ binary_mtime = binary_path.stat().st_mtime if binary_path.exists() else None
301
+ except OSError:
302
+ binary_mtime = None
303
+
304
+ if binary_mtime is None:
305
+ # Most machines never built a local tui/ binary — absent, not stale.
306
+ return CheckResult(
307
+ check_id="tui_binary",
308
+ scope="machine",
309
+ severity=Severity.OK,
310
+ headroom="not present on this machine",
311
+ values={**values, "present": False},
312
+ )
313
+
314
+ values["present"] = True
315
+ values["binary_mtime"] = binary_mtime
316
+
317
+ source_dir = resolve_tui_source_dir(ctx)
318
+ if source_dir is None:
319
+ return CheckResult(
320
+ check_id="tui_binary",
321
+ scope="machine",
322
+ severity=Severity.OK,
323
+ headroom="binary present (tui/ source tree not found to compare)",
324
+ values=values,
325
+ )
326
+
327
+ values["source_dir"] = str(source_dir)
328
+ source_mtime = _newest_rust_source_mtime(source_dir)
329
+ if source_mtime is None:
330
+ return CheckResult(
331
+ check_id="tui_binary",
332
+ scope="machine",
333
+ severity=Severity.OK,
334
+ headroom="binary present (tui/ source tree not found to compare)",
335
+ values=values,
336
+ )
337
+
338
+ values["source_mtime"] = source_mtime
339
+ if source_mtime > binary_mtime:
340
+ stale_hours = (source_mtime - binary_mtime) / 3600.0
341
+ return CheckResult(
342
+ check_id="tui_binary",
343
+ scope="machine",
344
+ severity=Severity.WARN,
345
+ headroom=f"binary is {stale_hours:.1f}h older than tui/ source",
346
+ detail="rebuild the tui/ binary — source changed since the last local build",
347
+ threshold="warn when source/ is newer than the built binary",
348
+ values=values,
349
+ )
350
+
351
+ return CheckResult(
352
+ check_id="tui_binary",
353
+ scope="machine",
354
+ severity=Severity.OK,
355
+ headroom="up to date with tui/ source",
356
+ values=values,
357
+ )
358
+
359
+
360
+ @check(
361
+ id="webapp_bundle",
362
+ scope="machine",
363
+ title="webapp bundle",
364
+ order=47,
365
+ description=(
366
+ "This machine's live `coord web --dist` bundle vs. the "
367
+ "coord/dashboard/webapp/ source tree it was supposedly built from — "
368
+ "feeds fleet_webapp_bundle (#1834 lane 5)."
369
+ ),
370
+ )
371
+ def probe_webapp_bundle(ctx: HealthContext) -> CheckResult:
372
+ """The webapp analogue of :func:`probe_tui_binary`: same shape, same
373
+ fail-soft-to-absent convention, different subject.
374
+
375
+ Not graded against ``OWN_VERSION`` or the released wheel — see this
376
+ module's docstring — so this only ever reports staleness relative to the
377
+ ``webapp/`` source tree, never a version.
378
+ """
379
+ dist_path = resolve_webapp_dist_path(ctx)
380
+ values: dict = {"path": str(dist_path)}
381
+
382
+ try:
383
+ present = dist_path.exists()
384
+ except OSError:
385
+ present = False
386
+
387
+ if not present:
388
+ # Most machines never run `coord web --dist` — absent, not stale.
389
+ return CheckResult(
390
+ check_id="webapp_bundle",
391
+ scope="machine",
392
+ severity=Severity.OK,
393
+ headroom="not present on this machine",
394
+ values={**values, "present": False},
395
+ )
396
+
397
+ values["present"] = True
398
+ try:
399
+ resolved = dist_path.resolve()
400
+ # `coord-web-dist-build.sh` names each release directory after the
401
+ # SHA it built — the symlink target's basename doubles as "which
402
+ # commit this machine is actually serving" whenever that convention
403
+ # holds, and is simply absent (not fabricated) when it doesn't.
404
+ values["sha"] = resolved.name
405
+ except OSError:
406
+ resolved = dist_path
407
+
408
+ try:
409
+ dist_mtime = resolved.stat().st_mtime
410
+ except OSError:
411
+ dist_mtime = None
412
+ values["dist_mtime"] = dist_mtime
413
+
414
+ source_dir = resolve_webapp_source_dir(ctx)
415
+ if source_dir is None or dist_mtime is None:
416
+ return CheckResult(
417
+ check_id="webapp_bundle",
418
+ scope="machine",
419
+ severity=Severity.OK,
420
+ headroom="bundle present (webapp/ source tree not found to compare)",
421
+ values=values,
422
+ )
423
+
424
+ values["source_dir"] = str(source_dir)
425
+ source_mtime = _newest_webapp_source_mtime(source_dir)
426
+ if source_mtime is None:
427
+ return CheckResult(
428
+ check_id="webapp_bundle",
429
+ scope="machine",
430
+ severity=Severity.OK,
431
+ headroom="bundle present (webapp/ source tree not found to compare)",
432
+ values=values,
433
+ )
434
+
435
+ values["source_mtime"] = source_mtime
436
+ if source_mtime > dist_mtime:
437
+ stale_hours = (source_mtime - dist_mtime) / 3600.0
438
+ return CheckResult(
439
+ check_id="webapp_bundle",
440
+ scope="machine",
441
+ severity=Severity.WARN,
442
+ headroom=f"bundle is {stale_hours:.1f}h older than webapp/ source",
443
+ detail=(
444
+ "coord-web-dist-build.timer has not published since this "
445
+ "source changed — check `systemctl --user status "
446
+ "coord-web-dist-build.timer` on this machine"
447
+ ),
448
+ threshold="warn when webapp/ source is newer than the live bundle",
449
+ values=values,
450
+ )
451
+
452
+ return CheckResult(
453
+ check_id="webapp_bundle",
454
+ scope="machine",
455
+ severity=Severity.OK,
456
+ headroom="up to date with webapp/ source",
457
+ values=values,
458
+ )
@@ -0,0 +1,99 @@
1
+ """Disk headroom per configured mount point (#1628).
2
+
3
+ The 2026-07-30 incident that motivated this milestone: elitebook's ``/home``
4
+ reached **0 bytes free** and every worker on that machine failed in a way
5
+ that looked like a coordinator bug. Nothing was watching the one number
6
+ that would have said so a day earlier.
7
+
8
+ Thresholds are expressed as percent *free* — headroom — rather than percent
9
+ used, because "how much is left" is the question, and because the rendered
10
+ line still shows used% for the operator who thinks in those terms.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import shutil
17
+
18
+ from coord.health.models import CheckResult, HealthContext, Severity
19
+ from coord.health.registry import check
20
+ from coord.health.units import expand, human_bytes
21
+
22
+
23
+ @check(
24
+ id="disk",
25
+ scope="machine",
26
+ title="disk",
27
+ order=10,
28
+ description="Free space on the filesystems the fleet writes to.",
29
+ )
30
+ def probe_disk(ctx: HealthContext) -> list[CheckResult]:
31
+ """One result per distinct filesystem behind ``thresholds.disk_paths``.
32
+
33
+ Paths that don't exist are skipped silently (``/home`` is not a separate
34
+ mount everywhere). Paths that resolve to a filesystem already reported
35
+ are skipped too — on a single-root machine ``/``, ``/home`` and
36
+ ``~/.coord`` are the same device, and three identical CRIT lines is noise
37
+ that trains an operator to skim.
38
+ """
39
+ th = ctx.thresholds
40
+ results: list[CheckResult] = []
41
+ seen_devices: set[int] = set()
42
+
43
+ for raw in th.disk_paths:
44
+ path = expand(raw, ctx.home)
45
+ try:
46
+ device = os.stat(path).st_dev
47
+ except OSError:
48
+ continue # not present on this machine — not a finding
49
+ if device in seen_devices:
50
+ continue
51
+ seen_devices.add(device)
52
+
53
+ try:
54
+ usage = shutil.disk_usage(str(path))
55
+ except OSError as exc:
56
+ results.append(
57
+ CheckResult(
58
+ check_id="disk",
59
+ scope="machine",
60
+ subject=str(raw),
61
+ severity=Severity.UNKNOWN,
62
+ headroom=f"could not stat filesystem: {exc}",
63
+ error=str(exc),
64
+ )
65
+ )
66
+ continue
67
+
68
+ total = float(usage.total)
69
+ free_pct = (usage.free / total * 100.0) if total > 0 else 0.0
70
+ used_pct = 100.0 - free_pct
71
+
72
+ if free_pct < th.disk_crit_free_pct:
73
+ severity = Severity.CRIT
74
+ elif free_pct < th.disk_warn_free_pct:
75
+ severity = Severity.WARN
76
+ else:
77
+ severity = Severity.OK
78
+
79
+ results.append(
80
+ CheckResult(
81
+ check_id="disk",
82
+ scope="machine",
83
+ subject=str(raw),
84
+ severity=severity,
85
+ headroom=f"{used_pct:.0f}% used ({human_bytes(usage.free)} free)",
86
+ threshold=f"crit at {100.0 - th.disk_crit_free_pct:.0f}%",
87
+ values={
88
+ "path": str(path),
89
+ "total_bytes": usage.total,
90
+ "free_bytes": usage.free,
91
+ "used_bytes": usage.used,
92
+ "free_pct": round(free_pct, 2),
93
+ "used_pct": round(used_pct, 2),
94
+ "warn_free_pct": th.disk_warn_free_pct,
95
+ "crit_free_pct": th.disk_crit_free_pct,
96
+ },
97
+ )
98
+ )
99
+ return results
@@ -0,0 +1,89 @@
1
+ """Fleet-scope: the board daemon's own ``/board`` latency + payload size (#1630, #1597).
2
+
3
+ A 5.3MB ``/board`` payload already broke ``coord report-result`` once (#1336)
4
+ by blowing a 5s prefetch timeout — this check exists so that regression shows
5
+ up as a fleet-health row instead of a mysterious timeout report weeks later.
6
+ Like the other fleet checks in this package, the daemon's ``/board`` handler
7
+ (``coord.serve_app``) is the thing that actually measures its own latency and
8
+ serialized body size on each rebuild; this probe only judges the numbers it's
9
+ handed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from coord.health.models import CheckResult, HealthContext, Severity
15
+ from coord.health.registry import check
16
+ from coord.health.units import human_bytes
17
+
18
+ # #1336: the 5.3MB payload that broke a 5s prefetch. Warn well before that.
19
+ _PAYLOAD_WARN_BYTES = 2 * 1024 * 1024
20
+ _PAYLOAD_CRIT_BYTES = 5 * 1024 * 1024
21
+ _LATENCY_WARN_MS = 1500.0
22
+ _LATENCY_CRIT_MS = 4000.0
23
+
24
+
25
+ @check(
26
+ id="fleet_board_latency",
27
+ scope="fleet",
28
+ title="board latency+size",
29
+ order=20,
30
+ description="The daemon's own /board rebuild latency and serialized payload size.",
31
+ )
32
+ def probe_board_latency(ctx: HealthContext) -> CheckResult:
33
+ if ctx.fleet is None:
34
+ return CheckResult(
35
+ check_id="fleet_board_latency",
36
+ scope="fleet",
37
+ severity=Severity.UNKNOWN,
38
+ headroom="no fleet snapshot (fleet checks only run on the daemon)",
39
+ )
40
+
41
+ dh = ctx.fleet.daemon_host or {}
42
+ latency_ms = dh.get("board_latency_ms")
43
+ payload_bytes = dh.get("board_payload_bytes")
44
+
45
+ if latency_ms is None and payload_bytes is None:
46
+ return CheckResult(
47
+ check_id="fleet_board_latency",
48
+ scope="fleet",
49
+ severity=Severity.UNKNOWN,
50
+ headroom="no /board build measured yet",
51
+ )
52
+
53
+ severity = Severity.OK
54
+ reasons: list[str] = []
55
+ if payload_bytes is not None:
56
+ if payload_bytes >= _PAYLOAD_CRIT_BYTES:
57
+ severity = Severity.CRIT
58
+ reasons.append(f"payload {human_bytes(payload_bytes)}")
59
+ elif payload_bytes >= _PAYLOAD_WARN_BYTES:
60
+ severity = Severity.WARN
61
+ reasons.append(f"payload {human_bytes(payload_bytes)}")
62
+ if latency_ms is not None:
63
+ if latency_ms >= _LATENCY_CRIT_MS:
64
+ severity = Severity.CRIT
65
+ reasons.append(f"latency {latency_ms:.0f}ms")
66
+ elif latency_ms >= _LATENCY_WARN_MS and severity is not Severity.CRIT:
67
+ severity = Severity.WARN
68
+ reasons.append(f"latency {latency_ms:.0f}ms")
69
+
70
+ parts = []
71
+ if latency_ms is not None:
72
+ parts.append(f"{latency_ms:.0f}ms")
73
+ if payload_bytes is not None:
74
+ parts.append(human_bytes(payload_bytes))
75
+ headroom = " / ".join(parts) if parts else "no data"
76
+ if severity is not Severity.OK:
77
+ headroom += f" ({', '.join(reasons)})"
78
+
79
+ return CheckResult(
80
+ check_id="fleet_board_latency",
81
+ scope="fleet",
82
+ severity=severity,
83
+ headroom=headroom,
84
+ threshold=(
85
+ f"warn at {human_bytes(_PAYLOAD_WARN_BYTES)}/{_LATENCY_WARN_MS:.0f}ms, "
86
+ f"crit at {human_bytes(_PAYLOAD_CRIT_BYTES)}/{_LATENCY_CRIT_MS:.0f}ms"
87
+ ),
88
+ values={"latency_ms": latency_ms, "payload_bytes": payload_bytes},
89
+ )