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
coord/goal.py ADDED
@@ -0,0 +1,125 @@
1
+ """#978: GOAL.md → Plans-panel north-star header projection.
2
+
3
+ `GOAL.md` (repo root) is the living cross-repo/cross-machine intent doc —
4
+ `coordinator.yml` is the source of truth for *topology*, GOAL.md is the
5
+ source of truth for *intent* (see the project's own CLAUDE.md). The coord-tui
6
+ Plans panel pins a short read-only excerpt of it above the plan roster.
7
+
8
+ **Fail-open, server-computed (mirrors `coord.plans` / #975-#976).** GOAL.md is
9
+ a repo-root doc, not shipped in the `coord` PyPI package (`pyproject.toml`
10
+ only packages `coord*`) — so it only exists on disk when `coord serve` is
11
+ running from an actual git checkout of this repo (the coordinator's own dev/
12
+ always-on host, typically an editable install). `read_goal_header()` never
13
+ raises: any failure to locate/read/parse the file returns
14
+ `{"available": False}`, and the coord-tui Plans panel simply renders without
15
+ the pinned header in that case — identical to today's behaviour, no
16
+ regression for older daemons or packaged installs.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from datetime import date
23
+ from pathlib import Path
24
+
25
+ # Matches the italic "_Last updated: 2026-07-04_" line convention used at the
26
+ # top of GOAL.md.
27
+ _LAST_UPDATED_RE = re.compile(r"_Last updated:\s*(\d{4}-\d{2}-\d{2})_")
28
+
29
+ # The "## \U0001F3AF North star" heading (or a plain "## North star" fallback)
30
+ # — matched case-insensitively so an emoji-less rewrite still resolves.
31
+ # Restricted to heading levels 2-6: GOAL.md's own H1 title
32
+ # ("# Current Goal — North Star") also contains the words "north star", and a
33
+ # level-1-inclusive match would bind here instead of the intended `## North
34
+ # star` section below it (#978 review) — the first bold text after the H1
35
+ # would then be the blockquote's framing sentence, not the real headline.
36
+ _NORTH_STAR_HEADING_RE = re.compile(r"^#{2,6}[^\n]*north star[^\n]*$", re.IGNORECASE | re.MULTILINE)
37
+
38
+ _BOLD_RE = re.compile(r"\*\*(.+?)\*\*")
39
+ _H1_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE)
40
+
41
+ _HEADLINE_MAX_LEN = 220
42
+
43
+
44
+ def _resolve_goal_md_path() -> Path | None:
45
+ """Best-effort location of this checkout's GOAL.md.
46
+
47
+ Reuses the same editable-checkout detection as `cli.py`'s
48
+ `_warn_if_editable_checkout_moved()`: `coord.__file__`'s grandparent
49
+ directory is the repo root only when running from a checkout (not a
50
+ `site-packages` install, which has no repo root at all). Returns `None`
51
+ when GOAL.md can't be resolved to an existing file.
52
+ """
53
+ try:
54
+ import coord as _coord # noqa: PLC0415
55
+
56
+ coord_file = _coord.__file__ or ""
57
+ if "site-packages" in coord_file:
58
+ return None # PyPI install — GOAL.md was never shipped with it.
59
+ repo_root = Path(coord_file).resolve().parents[1]
60
+ candidate = repo_root / "GOAL.md"
61
+ return candidate if candidate.is_file() else None
62
+ except Exception: # noqa: BLE001 — best-effort, never raise
63
+ return None
64
+
65
+
66
+ def parse_goal_header(text: str) -> dict:
67
+ """Pure parser: GOAL.md text -> the Plans-panel header projection.
68
+
69
+ Split out from `read_goal_header()` so tests can exercise parsing edge
70
+ cases (malformed/missing date, missing north-star section, ...) directly
71
+ against a string fixture, independent of the on-disk file-resolution
72
+ story above.
73
+ """
74
+ result: dict = {
75
+ "available": True,
76
+ "headline": "",
77
+ "last_updated": None,
78
+ "days_since_update": None,
79
+ }
80
+
81
+ m = _LAST_UPDATED_RE.search(text)
82
+ if m:
83
+ date_str = m.group(1)
84
+ result["last_updated"] = date_str
85
+ try:
86
+ parsed = date.fromisoformat(date_str)
87
+ result["days_since_update"] = (date.today() - parsed).days
88
+ except ValueError:
89
+ pass # leave days_since_update as None — an unparseable date still surfaces as text
90
+
91
+ # Headline: first bolded sentence under the "North star" heading — falls
92
+ # back to the document's H1 title so a reformatted GOAL.md still surfaces
93
+ # *something* rather than an empty pinned header.
94
+ headline = ""
95
+ heading = _NORTH_STAR_HEADING_RE.search(text)
96
+ if heading:
97
+ bold = _BOLD_RE.search(text[heading.end():])
98
+ if bold:
99
+ headline = bold.group(1).strip()
100
+ if not headline:
101
+ h1 = _H1_RE.search(text)
102
+ if h1:
103
+ headline = h1.group(1).strip()
104
+ if len(headline) > _HEADLINE_MAX_LEN:
105
+ headline = headline[: _HEADLINE_MAX_LEN - 1].rstrip() + "…"
106
+ result["headline"] = headline
107
+ return result
108
+
109
+
110
+ def read_goal_header() -> dict:
111
+ """#978: fail-open GOAL.md -> Plans-panel-header projection.
112
+
113
+ Returns `{"available": False}` when GOAL.md can't be located (packaged
114
+ install, no discoverable repo root, missing file) or read. Never raises —
115
+ called from `coord/serve_app.py`'s `board()` handler, where a failure
116
+ here must never blank the rest of the board payload.
117
+ """
118
+ try:
119
+ path = _resolve_goal_md_path()
120
+ if path is None:
121
+ return {"available": False}
122
+ text = path.read_text(encoding="utf-8")
123
+ return parse_goal_header(text)
124
+ except Exception: # noqa: BLE001 — best-effort, never raise
125
+ return {"available": False}
coord/graph_health.py ADDED
@@ -0,0 +1,348 @@
1
+ """Graphify knowledge-graph freshness + worktree-bootstrap health.
2
+
3
+ The repo ships a graphify graph in ``graphify-out/`` that agents are told to
4
+ query first (see CLAUDE.md). Two things silently break it, and neither is
5
+ visible from the code:
6
+
7
+ **1. Linked worktrees are graph-blind.** ``graphify-out/`` is gitignored by
8
+ design (only its ``.gitignore`` is tracked), so ``git worktree add`` produces
9
+ an empty one — and ``graphify query`` resolves ``graphify-out/graph.json``
10
+ strictly relative to cwd, with no upward walk and no ``--graph`` override.
11
+ ``.githooks/post-checkout`` fixes this by symlinking each entry of the base
12
+ checkout's graph (``graph.json``, ``manifest.json``, ``cache/``, ...) into
13
+ the worktree's ``graphify-out/`` — the directory itself, and its tracked
14
+ ``.gitignore``, are never touched (#1617: replacing the whole directory with
15
+ a symlink deleted the tracked ``.gitignore`` out from under git). This hook
16
+ only runs where ``core.hooksPath`` points at ``.githooks`` — a one-time,
17
+ per-machine ``git config`` that nothing enforces. :func:`hooks_path_status`
18
+ checks it.
19
+
20
+ **2. The graph drifts out of sync with HEAD.** graphify's own hooks are
21
+ best-effort and structurally cannot cover every ref-moving operation:
22
+
23
+ * ``post-commit``/``post-checkout``/``post-merge`` all ``exit 0`` during a
24
+ rebase, merge, or cherry-pick — so the merge agent's proactive rebase
25
+ (#306), the single most common ref move in the fleet, never rebuilds.
26
+ * ``git reset --hard`` fires no rebuild hook at all; git has none.
27
+ * Every hook failure path is ``exit 0``, and the rebuild itself is a detached
28
+ background process with a 600s ``SIGALRM`` timeout that logs to
29
+ ``~/.cache/graphify-rebuild.log`` — a timeout, an OOM, or an ENOENT from a
30
+ reaped worktree all fail invisibly.
31
+ * Concurrent triggers coalesce ("Rebuild already in progress — changes
32
+ queued").
33
+ * The hooks' own ``[ ! -f graphify-out/graph.json ] && exit 0`` guard is a
34
+ permanent off-switch: purge the graph once and they no-op forever.
35
+
36
+ So the hooks are an optimization, not the correctness mechanism. Correctness
37
+ comes from a cheap *check*, which is possible because ``GRAPH_REPORT.md``
38
+ records the commit it was built from::
39
+
40
+ - Built from commit: `5be69d08`
41
+
42
+ :func:`graph_status` compares that to HEAD. ``coord diagnose --graph``
43
+ surfaces it, so drift shows up in a routine health check instead of being
44
+ discovered by a confused agent mid-task.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import re
50
+ import subprocess
51
+ import time
52
+ from dataclasses import dataclass
53
+ from pathlib import Path
54
+
55
+ # ``- Built from commit: `5be69d08` `` in GRAPH_REPORT.md. This is the only
56
+ # machine-readable record of the graph's source commit — manifest.json holds
57
+ # per-file hashes, not a commit.
58
+ _BUILT_FROM_RE = re.compile(r"^-\s*Built from commit:\s*`([0-9a-fA-F]+)`", re.MULTILINE)
59
+
60
+ # The versioned hooks directory this repo expects core.hooksPath to point at.
61
+ HOOKS_PATH = ".githooks"
62
+
63
+
64
+ @dataclass
65
+ class GraphStatus:
66
+ """Freshness of one checkout's graphify graph."""
67
+
68
+ repo_path: Path
69
+ present: bool = False
70
+ # True when graphify-out/graph.json is a symlink — i.e. a worktree
71
+ # borrowing the base checkout's graph (the .githooks/post-checkout
72
+ # bootstrap ran). graphify-out/ itself is always a real directory (its
73
+ # tracked .gitignore has to survive — see #1617); only the entries
74
+ # inside it are symlinked.
75
+ is_symlink: bool = False
76
+ # The base checkout's graphify-out/ directory (graph.json's resolved
77
+ # parent), not graph.json itself — kept as a directory path so existing
78
+ # "owner checkout" math (``link_target.parent``) still lands on the base
79
+ # checkout root.
80
+ link_target: Path | None = None
81
+ built_sha: str | None = None
82
+ head_sha: str | None = None
83
+ in_sync: bool = False
84
+ age_seconds: float | None = None
85
+ # mtime of graphify-out/manifest.json — the last time graphify *checked* the
86
+ # graph against the working tree, whether or not it rewrote anything.
87
+ verified_at: float | None = None
88
+ # Commit timestamp of HEAD, to compare against verified_at.
89
+ head_committed_at: float | None = None
90
+ # Set when we could not determine freshness at all (no report, no git).
91
+ unknown_reason: str | None = None
92
+
93
+ @property
94
+ def stamp_behind(self) -> bool:
95
+ """``GRAPH_REPORT.md``'s "Built from commit" differs from HEAD."""
96
+ return bool(self.present and self.built_sha and self.head_sha and not self.in_sync)
97
+
98
+ @property
99
+ def verified_current(self) -> bool:
100
+ """graphify has checked the graph against the tree since HEAD landed.
101
+
102
+ ``graphify update`` re-extracts, compares topology, and when nothing
103
+ changed prints "No code-graph topology changes detected; outputs left
104
+ untouched" — it deliberately does NOT rewrite ``graph.json`` or
105
+ ``GRAPH_REPORT.md``, so the "Built from commit" stamp stays at whatever
106
+ HEAD was the last time the content actually changed. It *does* still
107
+ call ``save_manifest``, so ``manifest.json``'s mtime is the honest
108
+ record of "last verified".
109
+
110
+ Without this, a checkout whose graph is genuinely current but whose
111
+ stamp is behind would be reported STALE on every run, forever — a
112
+ health check that cries wolf is worse than none.
113
+ """
114
+ if self.verified_at is None or self.head_committed_at is None:
115
+ return False
116
+ return self.verified_at >= self.head_committed_at
117
+
118
+ @property
119
+ def stale(self) -> bool:
120
+ """The stamp is behind AND graphify has not verified the graph against
121
+ the tree since HEAD landed. Deliberately False when freshness is
122
+ *unknown* — an unknown is reported separately, not counted as drift."""
123
+ return self.stamp_behind and not self.verified_current
124
+
125
+
126
+ def read_built_sha(report_path: Path) -> str | None:
127
+ """The commit ``GRAPH_REPORT.md`` says the graph was built from."""
128
+ try:
129
+ text = report_path.read_text(encoding="utf-8", errors="replace")
130
+ except OSError:
131
+ return None
132
+ m = _BUILT_FROM_RE.search(text)
133
+ return m.group(1) if m else None
134
+
135
+
136
+ def _git_out(repo_path: Path, *args: str) -> str | None:
137
+ try:
138
+ r = subprocess.run(
139
+ ["git", *args],
140
+ cwd=str(repo_path),
141
+ capture_output=True,
142
+ text=True,
143
+ timeout=10.0,
144
+ )
145
+ except (subprocess.SubprocessError, OSError):
146
+ return None
147
+ if r.returncode != 0:
148
+ return None
149
+ return r.stdout.strip() or None
150
+
151
+
152
+ def _head_sha(repo_path: Path) -> str | None:
153
+ return _git_out(repo_path, "rev-parse", "HEAD")
154
+
155
+
156
+ def _head_committed_at(repo_path: Path) -> float | None:
157
+ raw = _git_out(repo_path, "log", "-1", "--format=%ct", "HEAD")
158
+ try:
159
+ return float(raw) if raw else None
160
+ except ValueError:
161
+ return None
162
+
163
+
164
+ def _shas_agree(built: str, head: str) -> bool:
165
+ """Compare on the shorter of the two — the report abbreviates (8 chars by
166
+ default) while ``git rev-parse HEAD`` is full-length."""
167
+ n = min(len(built), len(head))
168
+ if n < 4: # too short to be a meaningful comparison
169
+ return False
170
+ return built[:n].lower() == head[:n].lower()
171
+
172
+
173
+ def graph_status(repo_path: Path) -> GraphStatus:
174
+ """Freshness of the graphify graph for the checkout at *repo_path*.
175
+
176
+ Read-only and best-effort: a missing graph, a missing report, or a repo
177
+ git can't read all return a populated :class:`GraphStatus` with
178
+ ``unknown_reason`` set rather than raising.
179
+ """
180
+ st = GraphStatus(repo_path=repo_path)
181
+ out_dir = repo_path / "graphify-out"
182
+ graph_file = out_dir / "graph.json"
183
+
184
+ # A borrowed graph symlinks graph.json (and friends) individually;
185
+ # graphify-out/ itself is always a real directory (#1617).
186
+ st.is_symlink = graph_file.is_symlink()
187
+ if st.is_symlink:
188
+ try:
189
+ st.link_target = graph_file.resolve().parent
190
+ except OSError:
191
+ st.link_target = None
192
+
193
+ if not graph_file.is_file():
194
+ st.unknown_reason = "no graphify-out/graph.json (graph never built here)"
195
+ return st
196
+ st.present = True
197
+
198
+ try:
199
+ st.age_seconds = max(0.0, time.time() - graph_file.stat().st_mtime)
200
+ except OSError:
201
+ st.age_seconds = None
202
+
203
+ try:
204
+ st.verified_at = (out_dir / "manifest.json").stat().st_mtime
205
+ except OSError:
206
+ st.verified_at = None
207
+
208
+ st.built_sha = read_built_sha(out_dir / "GRAPH_REPORT.md")
209
+ # Freshness is always judged against the checkout that OWNS the graph. For
210
+ # a symlinked worktree that's the base checkout, not the worktree's own
211
+ # HEAD — the worktree is on a feature branch by definition and comparing
212
+ # against it would report permanent, meaningless drift.
213
+ owner = st.link_target.parent if (st.is_symlink and st.link_target) else repo_path
214
+ st.head_sha = _head_sha(owner)
215
+ st.head_committed_at = _head_committed_at(owner)
216
+
217
+ if not st.built_sha:
218
+ st.unknown_reason = "GRAPH_REPORT.md has no 'Built from commit' line"
219
+ elif not st.head_sha:
220
+ st.unknown_reason = f"could not read HEAD of {owner}"
221
+ else:
222
+ st.in_sync = _shas_agree(st.built_sha, st.head_sha)
223
+ return st
224
+
225
+
226
+ def hooks_path_status(repo_path: Path) -> tuple[bool, str]:
227
+ """``(ok, detail)`` for this checkout's ``core.hooksPath``.
228
+
229
+ The versioned ``.githooks/post-checkout`` bootstrap only runs when
230
+ ``core.hooksPath`` points at it — a one-time per-machine ``git config``.
231
+ Without it, worktrees on this machine stay graph-blind and nothing says so.
232
+ """
233
+ try:
234
+ r = subprocess.run(
235
+ ["git", "config", "--get", "core.hooksPath"],
236
+ cwd=str(repo_path),
237
+ capture_output=True,
238
+ text=True,
239
+ timeout=10.0,
240
+ )
241
+ except (subprocess.SubprocessError, OSError) as exc:
242
+ return False, f"could not read core.hooksPath ({exc})"
243
+ value = r.stdout.strip()
244
+ have_hook = (repo_path / HOOKS_PATH / "post-checkout").is_file()
245
+ if not value:
246
+ if not have_hook:
247
+ # The repo doesn't ship the bootstrap at all — telling the operator
248
+ # to point core.hooksPath at a directory that isn't there would
249
+ # silently disable ALL hooks for that checkout.
250
+ return False, (
251
+ f"no {HOOKS_PATH}/post-checkout in this repo — worktrees here get "
252
+ f"no linked graph (port the hook to this repo to enable it)"
253
+ )
254
+ return False, (
255
+ f"core.hooksPath is unset — worktrees on this machine will NOT get "
256
+ f"a linked graph. Fix: git -C {repo_path} config core.hooksPath {HOOKS_PATH}"
257
+ )
258
+ if Path(value).name != Path(HOOKS_PATH).name:
259
+ return False, (
260
+ f"core.hooksPath is {value!r}, expected {HOOKS_PATH!r} — the worktree "
261
+ f"graph bootstrap will not run"
262
+ )
263
+ if not have_hook:
264
+ return False, (
265
+ f"core.hooksPath={value} but {HOOKS_PATH}/post-checkout is missing "
266
+ f"(stale checkout?)"
267
+ )
268
+ orphaned = orphaned_hooks(repo_path)
269
+ if orphaned:
270
+ return False, (
271
+ f"core.hooksPath={value} but {', '.join(orphaned)} exist(s) only in "
272
+ f".git/hooks — git no longer runs them, so those graphify rebuilds "
273
+ f"are SILENTLY DISABLED. Add a shim in {HOOKS_PATH}/"
274
+ )
275
+ return True, f"core.hooksPath={value}"
276
+
277
+
278
+ def orphaned_hooks(repo_path: Path) -> list[str]:
279
+ """Hooks installed in the machine-local hooks dir with no counterpart in
280
+ :data:`HOOKS_PATH`.
281
+
282
+ Setting ``core.hooksPath`` makes git ignore ``.git/hooks`` **entirely** —
283
+ it does not merge or fall back. So any hook graphify installed there
284
+ (``post-commit``, ``post-checkout``, ``post-merge``) that has no shim in
285
+ the versioned directory stops running, with no error and no log line.
286
+ This shipped exactly once: only ``post-checkout`` had a shim, which
287
+ silently killed graphify's commit- and merge-triggered rebuilds.
288
+
289
+ Returns hook names, sorted. Empty when nothing is orphaned (including
290
+ when ``core.hooksPath`` isn't set — then ``.git/hooks`` is live and
291
+ there is nothing to orphan).
292
+ """
293
+ local_dir = _git_out(repo_path, "rev-parse", "--git-common-dir")
294
+ if not local_dir:
295
+ return []
296
+ common = Path(local_dir)
297
+ if not common.is_absolute():
298
+ common = repo_path / common
299
+ hooks_dir = common / "hooks"
300
+ versioned = repo_path / HOOKS_PATH
301
+ if not hooks_dir.is_dir() or not versioned.is_dir():
302
+ return []
303
+
304
+ out: list[str] = []
305
+ for entry in hooks_dir.iterdir():
306
+ name = entry.name
307
+ # graphify keeps .bak copies alongside; only real, executable hooks
308
+ # matter, and only ones git would actually invoke.
309
+ if name.endswith(".sample") or name.endswith(".bak") or "." in name:
310
+ continue
311
+ if not entry.is_file():
312
+ continue
313
+ if not (versioned / name).is_file():
314
+ out.append(name)
315
+ return sorted(out)
316
+
317
+
318
+ def format_status_lines(st: GraphStatus) -> list[str]:
319
+ """Human-readable report lines for *st* (used by ``coord diagnose --graph``)."""
320
+ lines: list[str] = []
321
+ where = str(st.repo_path)
322
+ if not st.present:
323
+ lines.append(f"✗ {where}: {st.unknown_reason}")
324
+ return lines
325
+
326
+ if st.is_symlink:
327
+ lines.append(f"↳ {where}/graphify-out → {st.link_target} (linked worktree)")
328
+
329
+ if st.unknown_reason:
330
+ lines.append(f"? {where}: freshness unknown — {st.unknown_reason}")
331
+ elif st.in_sync:
332
+ lines.append(f"✓ {where}: graph in sync (built from {st.built_sha})")
333
+ elif st.verified_current:
334
+ # Stamp behind, but graphify has re-checked the tree since HEAD landed
335
+ # and found no topology change — the graph content is current.
336
+ lines.append(
337
+ f"✓ {where}: graph content current — stamp says {st.built_sha} "
338
+ f"(HEAD {(st.head_sha or '')[:8]}), but verified against the tree "
339
+ f"since; graphify leaves outputs untouched when topology is unchanged"
340
+ )
341
+ else:
342
+ lines.append(
343
+ f"⚠ {where}: graph is STALE — built from {st.built_sha}, "
344
+ f"HEAD is {(st.head_sha or '')[:8]}"
345
+ )
346
+ if st.age_seconds is not None:
347
+ lines.append(f" graph.json age: {st.age_seconds / 3600.0:.1f}h")
348
+ return lines
@@ -0,0 +1,69 @@
1
+ """Fleet-health check engine (#1628).
2
+
3
+ Answers one question for a catalogue of fleet-degradation signals: **how much
4
+ headroom is left?** Runs on one machine, against that machine and the
5
+ checkouts ``coordinator.yml`` says live on it. No transport, no board state,
6
+ no renderer beyond ``coord health`` — those are H-3 and H-4.
7
+
8
+ The shape, in the order you'd meet it:
9
+
10
+ * ``models`` — :class:`~coord.health.models.CheckResult`, which carries raw
11
+ values **and** the rendered headroom string. Renderers never re-derive
12
+ severity from raw numbers; that rule is what keeps the logic from forking
13
+ once there is more than one surface.
14
+ * ``registry`` — the ``@check`` decorator and ``run_all``. Adding a check
15
+ means adding one module under ``checks/``; discovery is ``pkgutil`` over
16
+ that package, so nothing central needs editing.
17
+ * ``checks/`` — the seed probes: disk, cargo targets, worktrees, agent
18
+ install/version, claude binary, repo branch/dirt, graph freshness, plan
19
+ usage. Two of them (``graph``, ``plan_usage``) are thin wrappers over
20
+ ``coord.graph_health`` and ``coord.usage_limits`` rather than forks.
21
+ * ``render`` / ``cli`` — the one surface this child ships.
22
+
23
+ Every probe is cheap, read-only, and fails soft: a probe that raises becomes
24
+ an ``unknown`` result carrying the error text, never a failed run.
25
+ """
26
+
27
+ from coord.health.models import (
28
+ SCOPES,
29
+ CheckResult,
30
+ Checkout,
31
+ HealthContext,
32
+ Severity,
33
+ unknown_result,
34
+ worst,
35
+ )
36
+ from coord.health.registry import (
37
+ COST_CHEAP,
38
+ COST_NETWORK,
39
+ Check,
40
+ HealthReport,
41
+ all_checks,
42
+ check,
43
+ discover,
44
+ get,
45
+ register,
46
+ run_all,
47
+ run_check,
48
+ )
49
+
50
+ __all__ = [
51
+ "COST_CHEAP",
52
+ "COST_NETWORK",
53
+ "SCOPES",
54
+ "Check",
55
+ "CheckResult",
56
+ "Checkout",
57
+ "HealthContext",
58
+ "HealthReport",
59
+ "Severity",
60
+ "all_checks",
61
+ "check",
62
+ "discover",
63
+ "get",
64
+ "register",
65
+ "run_all",
66
+ "run_check",
67
+ "unknown_result",
68
+ "worst",
69
+ ]
@@ -0,0 +1,129 @@
1
+ """Fleet-wide health aggregation for the always-visible surfaces (#1631, H-4).
2
+
3
+ Consumes the ``fleet_health`` block H-3 computes
4
+ (:mod:`coord.health.fleet_snapshot`) — either straight off ``GET /board``
5
+ (thin client) or reassembled locally from :func:`coord.state.load_machine_health`
6
+ (host mode, no ``board_service`` configured, so there is no daemon process to
7
+ ask for the fleet-scope checks). Every unit counted here already carries its
8
+ own ``severity`` string, chosen by a probe upstream; this module only counts
9
+ and picks the worst — it never looks at a check's raw ``values``.
10
+
11
+ Two renderers consume :class:`FleetHealthSummary`: ``coord status``'s footer
12
+ (:func:`render_fleet_footer`, this module) and coord-tui's status-bar
13
+ indicator + detail overlay (``tui/src/app``, a from-scratch Rust port of the
14
+ same counting rule — see that crate's ``fleet_health`` module for the
15
+ mirrored logic and its own doc comment on why it can't just call this file).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import time
21
+ from dataclasses import dataclass
22
+
23
+ from coord.health.models import Severity
24
+
25
+ # Ascending severity order for the footer's non-OK enumeration — mirrors
26
+ # Severity.rank, so "FLEET: WARN 2, CRIT 1" lists the worst last (#1631's
27
+ # own example). OK is never listed here: zero non-OK units *is* "OK".
28
+ _FOOTER_ORDER = (Severity.UNKNOWN, Severity.WARN, Severity.CRIT)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class FleetHealthSummary:
33
+ """One aggregate verdict + per-severity counts.
34
+
35
+ One *unit* per machine (its own already-rolled-up ``severity`` — H-3's
36
+ ``_effective_severity``: unknown when stale/never-polled/offline, never
37
+ silently carried forward as green) plus one unit per fleet-scope check
38
+ (``fleet_checks``, each already a ``CheckResult.to_dict()``). Counting at
39
+ this granularity — not per individual per-machine check row — is what
40
+ keeps "3 machines, 8 checks each" from reading as "24 problems" when
41
+ really it's "1 machine is unhappy".
42
+ """
43
+
44
+ worst: Severity
45
+ counts: dict[str, int] # "ok"/"unknown"/"warn"/"crit" -> count
46
+ unit_count: int # total machines + fleet checks contributing
47
+
48
+
49
+ def _severity_of(value: "str | None") -> Severity:
50
+ try:
51
+ return Severity(value or "unknown")
52
+ except ValueError:
53
+ return Severity.UNKNOWN
54
+
55
+
56
+ def summarize_fleet_health(fleet_health: "dict | None") -> FleetHealthSummary:
57
+ """Aggregate a ``fleet_health`` block into one :class:`FleetHealthSummary`.
58
+
59
+ *fleet_health* is either the dict at ``/board``'s ``fleet_health`` key
60
+ (schema: ``{"machine_health": [...], "fleet_checks": [...]}``, see
61
+ ``coord.health.fleet_snapshot.FleetHealthSnapshot.to_dict``) or the
62
+ locally-assembled equivalent from :func:`local_fleet_health_block`.
63
+ ``None``/missing keys degrade to "no units" (``worst=OK``, all counts
64
+ zero) rather than raising — a daemon predating #1630 simply never sends
65
+ this key.
66
+ """
67
+ counts = {"ok": 0, "warn": 0, "crit": 0, "unknown": 0}
68
+ worst = Severity.OK
69
+ block = fleet_health or {}
70
+ machine_health = block.get("machine_health") or []
71
+ fleet_checks = block.get("fleet_checks") or []
72
+ units = [m.get("severity") for m in machine_health] + [
73
+ c.get("severity") for c in fleet_checks
74
+ ]
75
+ for raw in units:
76
+ sev = _severity_of(raw)
77
+ counts[sev.value] += 1
78
+ if sev.rank > worst.rank:
79
+ worst = sev
80
+ return FleetHealthSummary(worst=worst, counts=counts, unit_count=len(units))
81
+
82
+
83
+ def render_fleet_footer(summary: FleetHealthSummary) -> str:
84
+ """``FLEET: <state> (coord health for detail)`` — the ``coord status``
85
+ footer.
86
+
87
+ ``OK`` states its OK-ness rather than printing nothing (#1631's own
88
+ framing: silence is indistinguishable from a broken check). Always
89
+ returns a non-empty string — callers should always print it, never
90
+ conditionally skip it.
91
+ """
92
+ if summary.worst is Severity.OK:
93
+ state = "OK"
94
+ else:
95
+ parts = [
96
+ f"{sev.label} {summary.counts[sev.value]}"
97
+ for sev in _FOOTER_ORDER
98
+ if summary.counts[sev.value]
99
+ ]
100
+ state = ", ".join(parts) if parts else "OK"
101
+ return f"FLEET: {state} (coord health for detail)"
102
+
103
+
104
+ def local_fleet_health_block(machine_names: "list[str]") -> dict:
105
+ """Best-effort ``fleet_health``-shaped block for host mode (no
106
+ ``board_service`` configured — see ``coord.commands.status.status``).
107
+
108
+ Per-machine severities come straight from the local DB's
109
+ ``machine_health`` table (written by whichever ``coord serve`` tick loop
110
+ last ran on this host) via the SAME row-assembly H-3's daemon-side
111
+ projection uses (:func:`coord.health.fleet_snapshot._machine_health_rows`)
112
+ — so a machine that never reported reads ``unknown`` here exactly like it
113
+ does on ``/board``, not silently green.
114
+
115
+ ``fleet_checks`` is always empty on this path: those probes (board
116
+ latency, phantom-running rows, deploy-lane skew, …) only exist inside a
117
+ live ``coord serve`` process's in-memory ``FleetHealthRefresher`` — a
118
+ separate ``coord status`` invocation has no way to reach into that
119
+ process's memory, and re-running the probes here would mean duplicating
120
+ daemon-only, subprocess-heavy fact-gathering into a read path that must
121
+ stay cheap. A thin client (``board_service`` configured) gets the full
122
+ picture via ``GET /board`` instead.
123
+ """
124
+ from coord.health.fleet_snapshot import _machine_health_rows
125
+ from coord.state import load_machine_health
126
+
127
+ raw = load_machine_health()
128
+ rows = _machine_health_rows(machine_names, raw, now=time.time())
129
+ return {"machine_health": rows, "fleet_checks": []}
@@ -0,0 +1,13 @@
1
+ """Seed health checks (#1628).
2
+
3
+ **This file imports nothing on purpose.** ``coord.health.registry.discover``
4
+ walks this package with :func:`pkgutil.iter_modules` and imports every module
5
+ it finds, so a new check is installed by dropping ``coord/health/checks/
6
+ <name>.py`` here and decorating its probe with ``@check(...)`` — no edit to
7
+ this ``__init__``, no edit to a renderer, no edit to the CLI.
8
+
9
+ ``tests/test_health_registry.py::test_adding_a_check_touches_only_its_own_module``
10
+ enforces that: it writes a throwaway module into this directory, asserts it
11
+ shows up in the report and the JSON, and asserts the renderer/CLI/registry
12
+ files are byte-identical before and after.
13
+ """