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,280 @@
1
+ """How the agent's ``claude-coordinator`` is installed, and how current (#1628).
2
+
3
+ Two checks, one shared ``pip show`` call:
4
+
5
+ ``agent_venv``
6
+ The agent venv must be a **PyPI install**, not an editable one. An
7
+ editable agent runs whatever is checked out in someone's source tree —
8
+ including a half-finished feature branch — which makes the machine's
9
+ behaviour untraceable to any release. #1182 is the recorded version of
10
+ this going wrong (a stale non-editable install silently evaluating
11
+ retired logic and producing a false merge-gate block); an *editable*
12
+ agent is the same failure with no version number to blame it on.
13
+
14
+ ``agent_version``
15
+ How many released versions the install is behind. The comparison is
16
+ against **PyPI's simple index**, not the JSON API — see
17
+ ``coord.health.pypi`` for why that distinction is load-bearing rather
18
+ than pedantic.
19
+
20
+ Editable detection uses the ``Editable project location:`` line ``pip show``
21
+ prints only for editable installs; a PyPI install has just ``Location:``
22
+ pointing into site-packages.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ from coord.dist_name import CANDIDATE_NAMES
32
+ from coord.health.models import CheckResult, HealthContext, Severity
33
+ from coord.health.registry import COST_NETWORK, check
34
+ from coord.health.units import expand, shorten_path
35
+
36
+ #: #2103: kept only for messages that need "a name" rather than "the
37
+ #: installed name" (e.g. the ``description=`` strings below, which are
38
+ #: static and evaluated before any probe runs). Everything that actually
39
+ #: probes the venv tries every name in :data:`CANDIDATE_NAMES`, preferring
40
+ #: whichever the rename (#2096) is currently shipping.
41
+ PROJECT = CANDIDATE_NAMES[0]
42
+
43
+ # Where install-agent.sh puts the agent's venv. Overridable via
44
+ # `health.agent_venv_python`.
45
+ _DEFAULT_AGENT_VENV = "~/.coord-venv/bin/python3"
46
+
47
+
48
+ def resolve_agent_python(ctx: HealthContext) -> Path:
49
+ """The interpreter whose environment we're reporting on.
50
+
51
+ Configured value wins; otherwise the standard agent venv when it exists;
52
+ otherwise the running interpreter (which is the honest answer on a
53
+ coordinator-only box that never installed an agent venv).
54
+ """
55
+ configured = getattr(ctx.thresholds, "agent_venv_python", None)
56
+ if configured:
57
+ return expand(configured, ctx.home)
58
+ candidate = expand(_DEFAULT_AGENT_VENV, ctx.home)
59
+ if candidate.exists():
60
+ return candidate
61
+ return Path(sys.executable)
62
+
63
+
64
+ def pip_show(
65
+ python: Path, names: tuple[str, ...] = CANDIDATE_NAMES, *, timeout: float = 8.0
66
+ ) -> dict[str, str]:
67
+ """Parse ``<python> -m pip show <name>`` into a field dict, trying each
68
+ of *names* in order and returning the first that resolves (#2103).
69
+
70
+ The agent venv this probes may have either ``code-coordinator`` (the
71
+ name since #2104) or the pre-rename ``claude-coordinator`` installed,
72
+ depending which side of the #2096 rename it's on — a bare single-name
73
+ ``pip show`` would report "not installed" for one of them. The dict's
74
+ ``Name`` field (``pip show`` always prints one) tells the caller which
75
+ one actually matched.
76
+
77
+ Returns ``{}`` when pip isn't there or neither name is installed.
78
+ Raises only for genuinely unexpected conditions — the probe wrapping this
79
+ fails soft either way.
80
+ """
81
+ for name in names:
82
+ result = subprocess.run(
83
+ [str(python), "-m", "pip", "show", name],
84
+ capture_output=True,
85
+ text=True,
86
+ timeout=timeout,
87
+ )
88
+ if result.returncode != 0:
89
+ continue
90
+ fields = _parse_pip_show(result.stdout)
91
+ if fields:
92
+ return fields
93
+ return {}
94
+
95
+
96
+ def _parse_pip_show(stdout: str) -> dict[str, str]:
97
+ fields: dict[str, str] = {}
98
+ for line in stdout.splitlines():
99
+ key, sep, value = line.partition(":")
100
+ if sep:
101
+ fields[key.strip()] = value.strip()
102
+ return fields
103
+
104
+
105
+ @check(
106
+ id="agent_venv",
107
+ scope="machine",
108
+ title="agent venv",
109
+ order=40,
110
+ description="The agent's coordinator install is a PyPI install, not editable.",
111
+ )
112
+ def probe_agent_venv(ctx: HealthContext) -> CheckResult:
113
+ python = resolve_agent_python(ctx)
114
+ try:
115
+ fields = pip_show(python)
116
+ except (OSError, subprocess.SubprocessError) as exc:
117
+ return CheckResult(
118
+ check_id="agent_venv",
119
+ scope="machine",
120
+ severity=Severity.UNKNOWN,
121
+ headroom=f"could not run pip show ({type(exc).__name__})",
122
+ error=str(exc),
123
+ values={"python": str(python)},
124
+ )
125
+
126
+ if not fields:
127
+ tried = " or ".join(CANDIDATE_NAMES)
128
+ return CheckResult(
129
+ check_id="agent_venv",
130
+ scope="machine",
131
+ severity=Severity.UNKNOWN,
132
+ headroom=f"neither {tried} installed for {shorten_path(str(python), str(ctx.home))}",
133
+ error="pip show returned nothing",
134
+ values={"python": str(python)},
135
+ )
136
+
137
+ editable_location = fields.get("Editable project location") or ""
138
+ version = fields.get("Version", "")
139
+ location = fields.get("Location", "")
140
+
141
+ if editable_location:
142
+ return CheckResult(
143
+ check_id="agent_venv",
144
+ scope="machine",
145
+ severity=Severity.CRIT,
146
+ headroom=f"editable {version or '?'} from {shorten_path(editable_location, str(ctx.home))}",
147
+ detail=(
148
+ "an editable agent runs whatever is checked out in that tree — "
149
+ "its behaviour is not traceable to any release"
150
+ ),
151
+ threshold="crit when editable",
152
+ values={
153
+ "python": str(python),
154
+ "version": version,
155
+ "editable": True,
156
+ "editable_location": editable_location,
157
+ "location": location,
158
+ },
159
+ )
160
+
161
+ return CheckResult(
162
+ check_id="agent_venv",
163
+ scope="machine",
164
+ severity=Severity.OK,
165
+ headroom=f"pypi {version or '?'}",
166
+ detail=shorten_path(location, str(ctx.home)) if location else "",
167
+ values={
168
+ "python": str(python),
169
+ "version": version,
170
+ "editable": False,
171
+ "editable_location": None,
172
+ "location": location,
173
+ },
174
+ )
175
+
176
+
177
+ @check(
178
+ id="agent_version",
179
+ scope="machine",
180
+ title="agent version",
181
+ order=41,
182
+ cost=COST_NETWORK,
183
+ description="Installed coordinator version vs the latest release on PyPI's simple index.",
184
+ )
185
+ def probe_agent_version(ctx: HealthContext) -> CheckResult:
186
+ from coord.health.pypi import latest_release, parse_version # noqa: PLC0415
187
+
188
+ th = ctx.thresholds
189
+ python = resolve_agent_python(ctx)
190
+ try:
191
+ fields = pip_show(python)
192
+ except (OSError, subprocess.SubprocessError) as exc:
193
+ fields = {}
194
+ installed_raw = ""
195
+ pip_error: str | None = f"{type(exc).__name__}: {exc}"
196
+ else:
197
+ installed_raw = fields.get("Version", "")
198
+ pip_error = None
199
+
200
+ if not installed_raw:
201
+ return CheckResult(
202
+ check_id="agent_version",
203
+ scope="machine",
204
+ severity=Severity.UNKNOWN,
205
+ headroom="installed version unknown",
206
+ error=pip_error or "pip show reported no Version",
207
+ values={"python": str(python)},
208
+ )
209
+
210
+ # #2103: query PyPI for whichever name `pip show` actually matched, not
211
+ # a hardcoded one — `code-coordinator` and `claude-coordinator` are
212
+ # separate PyPI projects with separate release histories, and comparing
213
+ # a `code-coordinator` install's version against `claude-coordinator`'s
214
+ # index would compute nonsense skew.
215
+ installed_project = fields.get("Name") or PROJECT
216
+
217
+ try:
218
+ latest, finals = latest_release(
219
+ installed_project,
220
+ index_url=th.pypi_index_url,
221
+ timeout=th.network_timeout_secs,
222
+ )
223
+ except Exception as exc: # noqa: BLE001 — any network/parse failure is "unknown"
224
+ return CheckResult(
225
+ check_id="agent_version",
226
+ scope="machine",
227
+ severity=Severity.UNKNOWN,
228
+ headroom=f"installed {installed_raw}, PyPI index unreachable",
229
+ error=f"{type(exc).__name__}: {exc}",
230
+ values={
231
+ "python": str(python),
232
+ "installed": installed_raw,
233
+ "project": installed_project,
234
+ },
235
+ )
236
+
237
+ installed = parse_version(installed_raw)
238
+ if installed is None or latest is None:
239
+ return CheckResult(
240
+ check_id="agent_version",
241
+ scope="machine",
242
+ severity=Severity.UNKNOWN,
243
+ headroom=f"installed {installed_raw}, could not compare against the index",
244
+ error="unparseable version",
245
+ values={"python": str(python), "installed": installed_raw},
246
+ )
247
+
248
+ behind = sum(1 for v in finals if v > installed)
249
+ if behind >= th.agent_version_crit_behind:
250
+ severity = Severity.CRIT
251
+ elif behind >= th.agent_version_warn_behind:
252
+ severity = Severity.WARN
253
+ else:
254
+ severity = Severity.OK
255
+
256
+ if behind == 0:
257
+ headroom = f"{installed_raw} (latest {latest.raw})"
258
+ else:
259
+ headroom = (
260
+ f"{installed_raw}, {behind} release{'s' if behind != 1 else ''} behind "
261
+ f"(latest {latest.raw})"
262
+ )
263
+
264
+ return CheckResult(
265
+ check_id="agent_version",
266
+ scope="machine",
267
+ severity=severity,
268
+ headroom=headroom,
269
+ threshold=f"crit at {th.agent_version_crit_behind} behind",
270
+ values={
271
+ "python": str(python),
272
+ "installed": installed_raw,
273
+ "project": installed_project,
274
+ "latest": latest.raw,
275
+ "releases_behind": behind,
276
+ "index_url": th.pypi_index_url,
277
+ "warn_behind": th.agent_version_warn_behind,
278
+ "crit_behind": th.agent_version_crit_behind,
279
+ },
280
+ )
@@ -0,0 +1,171 @@
1
+ """Total size of every cargo ``target/`` directory this machine keeps (#1628).
2
+
3
+ The 2026-07-30 incident: **78G** of cargo build artifacts across a handful of
4
+ checkouts plus ``~/.coord/cargo-target`` — which is what actually consumed
5
+ ``/home``. ``coord.cargo_cache`` already GCs the *shared* cache (default 20
6
+ GiB cap), but nothing totals it together with the per-checkout ``target/``
7
+ dirs a human created by building in a live checkout, and that sum is the
8
+ number that fills a disk.
9
+
10
+ Cost note: this is the one seed probe that can be genuinely slow, because
11
+ "how big is 78G of small files" is a full tree walk. It is therefore
12
+ budgeted (``health.cargo_scan_budget_secs``, default 1.5s) and reports a
13
+ partial scan when it runs out. A partial total is a **lower bound**, so a
14
+ CRIT derived from one is still correct; only an OK is downgraded (to
15
+ ``unknown``) when the scan didn't finish, because "we didn't finish looking"
16
+ must never render as "nothing there".
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import time
23
+ from pathlib import Path
24
+
25
+ from coord.health.models import CheckResult, HealthContext, Severity
26
+ from coord.health.registry import check
27
+ from coord.health.units import expand, gib, human_bytes, shorten_path
28
+
29
+
30
+ def _dir_size_budgeted(path: Path, deadline: float) -> tuple[int, bool]:
31
+ """``(bytes, complete)`` for the regular files under *path*.
32
+
33
+ Symlinked subdirectories are never followed — a worktree's ``target``
34
+ symlinked at the shared cache must not be counted twice, and a symlink
35
+ out of the tree must not be counted at all.
36
+ """
37
+ total = 0
38
+ complete = True
39
+ checked = 0
40
+ for root, dirnames, filenames in os.walk(path, onerror=lambda _e: None):
41
+ dirnames[:] = [d for d in dirnames if not os.path.islink(os.path.join(root, d))]
42
+ for name in filenames:
43
+ try:
44
+ total += os.lstat(os.path.join(root, name)).st_size
45
+ except OSError:
46
+ continue
47
+ # time.monotonic() per file would dominate the walk on a tree with
48
+ # a million small files; per 200 entries is accurate enough for a
49
+ # 1.5s budget and costs nothing.
50
+ checked += len(filenames) + 1
51
+ if checked >= 200:
52
+ checked = 0
53
+ if time.monotonic() >= deadline:
54
+ complete = False
55
+ break
56
+ return total, complete
57
+
58
+
59
+ def _candidate_dirs(ctx: HealthContext) -> list[Path]:
60
+ """Every cargo target dir we know about, deduped, existing only.
61
+
62
+ Three sources, in report order:
63
+
64
+ 1. ``~/.coord/cargo-target/<repo>`` — the shared per-machine cache
65
+ (``coord.cargo_cache``), one subdirectory per repo.
66
+ 2. ``<checkout>/target`` for each checkout in ``coordinator.yml`` — what
67
+ a human building in the live checkout creates, invisible to the cache
68
+ GC.
69
+ 3. ``health.cargo_target_extra_dirs`` — anything else on this box.
70
+ """
71
+ from coord.cargo_cache import CACHE_DIRNAME # noqa: PLC0415
72
+
73
+ out: list[Path] = []
74
+ seen: set[Path] = set()
75
+
76
+ def _add(p: Path) -> None:
77
+ try:
78
+ resolved = p.resolve()
79
+ except OSError:
80
+ resolved = p
81
+ if resolved in seen or not p.is_dir():
82
+ return
83
+ seen.add(resolved)
84
+ out.append(p)
85
+
86
+ cache_root = ctx.coord_dir / CACHE_DIRNAME
87
+ try:
88
+ for entry in sorted(cache_root.iterdir()):
89
+ if entry.is_dir() and not entry.is_symlink():
90
+ _add(entry)
91
+ except OSError:
92
+ pass
93
+
94
+ for checkout in ctx.checkouts:
95
+ _add(checkout.path / "target")
96
+
97
+ for raw in getattr(ctx.thresholds, "cargo_target_extra_dirs", ()) or ():
98
+ _add(expand(raw, ctx.home))
99
+
100
+ return out
101
+
102
+
103
+ @check(
104
+ id="cargo_targets",
105
+ scope="machine",
106
+ title="cargo targets",
107
+ order=20,
108
+ description="Total size of cargo build artifacts across known target dirs.",
109
+ )
110
+ def probe_cargo_targets(ctx: HealthContext) -> CheckResult | None:
111
+ """One machine-scope result: the total, with the biggest offenders named."""
112
+ th = ctx.thresholds
113
+ dirs = _candidate_dirs(ctx)
114
+ if not dirs:
115
+ return None # no Rust on this box — silence beats a green line
116
+
117
+ deadline = time.monotonic() + max(0.05, float(th.cargo_scan_budget_secs))
118
+ sizes: list[tuple[Path, int]] = []
119
+ complete = True
120
+ for d in dirs:
121
+ size, done = _dir_size_budgeted(d, deadline)
122
+ sizes.append((d, size))
123
+ if not done:
124
+ complete = False
125
+ break
126
+ # Directories we never reached at all still count as "didn't finish".
127
+ if len(sizes) < len(dirs):
128
+ complete = False
129
+
130
+ total = sum(s for _, s in sizes)
131
+ total_gb = gib(total)
132
+
133
+ if total_gb > th.cargo_target_crit_gb:
134
+ severity = Severity.CRIT
135
+ elif total_gb > th.cargo_target_warn_gb:
136
+ severity = Severity.WARN
137
+ elif complete:
138
+ severity = Severity.OK
139
+ else:
140
+ # Below WARN but we stopped early: the real total could be anything.
141
+ severity = Severity.UNKNOWN
142
+
143
+ biggest = sorted(sizes, key=lambda pair: pair[1], reverse=True)[:3]
144
+ breakdown = ", ".join(
145
+ f"{shorten_path(str(p), str(ctx.home))} {human_bytes(s)}" for p, s in biggest if s > 0
146
+ )
147
+ headroom = human_bytes(total)
148
+ if breakdown:
149
+ headroom = f"{headroom} ({breakdown})"
150
+ if not complete:
151
+ headroom = f"{headroom} [partial scan — {th.cargo_scan_budget_secs}s budget hit]"
152
+
153
+ return CheckResult(
154
+ check_id="cargo_targets",
155
+ scope="machine",
156
+ severity=severity,
157
+ headroom=headroom,
158
+ threshold=f"crit at {th.cargo_target_crit_gb:.0f}G",
159
+ error=None if complete else "scan budget exhausted; total is a lower bound",
160
+ values={
161
+ "total_bytes": total,
162
+ "total_gb": round(total_gb, 2),
163
+ "complete": complete,
164
+ "dirs": [
165
+ {"path": str(p), "bytes": s, "gb": round(gib(s), 2)}
166
+ for p, s in sorted(sizes, key=lambda pair: pair[1], reverse=True)
167
+ ],
168
+ "warn_gb": th.cargo_target_warn_gb,
169
+ "crit_gb": th.cargo_target_crit_gb,
170
+ },
171
+ )
@@ -0,0 +1,65 @@
1
+ """Is the ``claude`` binary actually resolvable where the agent runs? (#1628)
2
+
3
+ This is the #859 failure in health-check form. ``coord-serve`` runs under
4
+ ``systemd --user`` with an empty ``Environment=``, so a bare ``claude`` on
5
+ ``$PATH`` resolves fine in an interactive shell and not at all in the daemon
6
+ — which is why :func:`coord.test_orchestrator.resolve_claude_bin` exists.
7
+ That resolution is reused verbatim here rather than re-derived: if the
8
+ resolver's rules ever change, this check must change with them, and the only
9
+ way to guarantee that is to call it.
10
+
11
+ A machine whose ``claude`` is missing is not degraded, it is *useless* — it
12
+ will accept dispatches and fail every one — so this is CRIT with no warn
13
+ level, exactly as the issue's table specifies.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+
20
+ from coord.health.models import CheckResult, HealthContext, Severity
21
+ from coord.health.registry import check
22
+ from coord.health.units import shorten_path
23
+
24
+
25
+ @check(
26
+ id="claude_binary",
27
+ scope="machine",
28
+ title="claude binary",
29
+ order=50,
30
+ description="The claude binary resolves and is executable for the agent.",
31
+ )
32
+ def probe_claude_binary(ctx: HealthContext) -> CheckResult:
33
+ from coord.test_orchestrator import resolve_claude_bin # noqa: PLC0415
34
+
35
+ path = resolve_claude_bin()
36
+ shown = shorten_path(path, str(ctx.home))
37
+
38
+ exists = os.path.exists(path)
39
+ executable = exists and os.access(path, os.X_OK)
40
+
41
+ if executable:
42
+ severity, headroom = Severity.OK, shown
43
+ elif exists:
44
+ severity, headroom = Severity.CRIT, f"{shown} is not executable"
45
+ else:
46
+ severity, headroom = Severity.CRIT, f"{shown} does not exist"
47
+
48
+ return CheckResult(
49
+ check_id="claude_binary",
50
+ scope="machine",
51
+ severity=severity,
52
+ headroom=headroom,
53
+ threshold="crit when unresolvable",
54
+ detail=(
55
+ ""
56
+ if executable
57
+ else "every dispatch to this machine will fail; set $CLAUDE_BIN or reinstall"
58
+ ),
59
+ values={
60
+ "path": path,
61
+ "exists": exists,
62
+ "executable": executable,
63
+ "claude_bin_env": os.environ.get("CLAUDE_BIN"),
64
+ },
65
+ )