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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""Fleet-scope: are the four deploy lanes running the same release? (#1630)
|
|
2
|
+
|
|
3
|
+
The four lanes named in the issue: each agent's ``~/.coord-venv`` (N of
|
|
4
|
+
them), the ``coord-serve`` process on the daemon host, the operator's
|
|
5
|
+
``~/.coord-cli-venv``, and the locally-built ``tui/`` binary. The CLI venv
|
|
6
|
+
lane exists *specifically* because it was found three releases stale on
|
|
7
|
+
2026-07-29 — silently driving `coord` commands without fixes everyone
|
|
8
|
+
believed were live.
|
|
9
|
+
|
|
10
|
+
This module never probes the filesystem itself. Two different sources feed
|
|
11
|
+
it, and #1806 is precisely about not conflating them:
|
|
12
|
+
|
|
13
|
+
* ``coord-serve``'s own version is a genuinely daemon-host-local fact (it can
|
|
14
|
+
only be introspected from the process actually running it) — the daemon's
|
|
15
|
+
health-poll tick gathers that into ``HealthContext.fleet.daemon_host``
|
|
16
|
+
(``coord.serve_app``'s ``FleetHealthRefresher``).
|
|
17
|
+
* The CLI venv's version is *not* a daemon-host fact — it's whichever
|
|
18
|
+
machine's filesystem the operator actually put ``~/.coord-cli-venv`` on,
|
|
19
|
+
which is very often a different box from the one running ``coord-serve``.
|
|
20
|
+
That fact rides the same transport every agent's ``~/.coord-venv`` already
|
|
21
|
+
does: each machine's own ``/health`` poll, via the ``cli_venv`` machine-
|
|
22
|
+
scope check in :mod:`coord.health.checks.deploy_lane_facts`. See #1806.
|
|
23
|
+
|
|
24
|
+
#1834 added a fifth kind of lane, and it is the one that makes this check
|
|
25
|
+
worth running: ``<unit> spawns (<machine>)`` — the version each *running*
|
|
26
|
+
coord service would actually hand its subprocesses, resolved from the live
|
|
27
|
+
process's own PATH. Every lane above measures an install; on 2026-08-04 all
|
|
28
|
+
of them agreed on 0.4.105 while the daemon spawned 0.4.103, because the
|
|
29
|
+
defect was not in any install but in what ``shutil.which("coord")`` found
|
|
30
|
+
first. Skew is only ever visible as a *relationship between* lanes, which is
|
|
31
|
+
why it is checked here and not by any amount of per-lane staleness logic.
|
|
32
|
+
|
|
33
|
+
Both checks below are fail-soft toward UNKNOWN, never toward OK: a lane no
|
|
34
|
+
machine has data for (an agent that never reported, a CLI venv that was
|
|
35
|
+
never configured anywhere) must not read as "in sync" just because there was
|
|
36
|
+
nothing to disagree with.
|
|
37
|
+
|
|
38
|
+
A third fleet check, ``fleet_webapp_bundle``, aggregates ``webapp_bundle``
|
|
39
|
+
(#1834 lane 5, the phone-webapp bundle `coord web --dist` serves) the same
|
|
40
|
+
way ``fleet_tui_binary`` aggregates ``tui_binary`` — same source
|
|
41
|
+
(:mod:`coord.health.checks.deploy_lane_facts`), same shape, same fail-soft
|
|
42
|
+
convention. It is deliberately **not** folded into the version-skew map
|
|
43
|
+
below: ``deploy/coord-web-dist-build.timer`` publishes continuously off
|
|
44
|
+
``origin/main``'s SHA, decoupled on purpose from the pip release cadence
|
|
45
|
+
every other lane in this module compares against, so a bundle's "version" is
|
|
46
|
+
never comparable to ``coord-serve``'s or an agent venv's. What it CAN
|
|
47
|
+
report — staleness against its own source tree, and disagreement between
|
|
48
|
+
machines about which bundle is live — it does, on its own terms, exactly
|
|
49
|
+
like ``tui_binary`` already does for the same reason.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
55
|
+
from coord.health.registry import check
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _machine_check_values(ctx: HealthContext, check_id: str) -> dict[str, dict]:
|
|
59
|
+
"""machine_name -> that machine's reported ``values`` for *check_id*.
|
|
60
|
+
|
|
61
|
+
Only includes machines whose result for this check exists and didn't
|
|
62
|
+
error — a machine that never runs/reports this check is simply absent
|
|
63
|
+
from the returned dict (the right shape for "optional lane" checks like
|
|
64
|
+
``cli_venv``/``tui_binary``, where most machines legitimately have
|
|
65
|
+
nothing to say). ``_agent_lane_versions`` below wants the opposite shape
|
|
66
|
+
— every machine present, ``None`` standing in for "no data" — because
|
|
67
|
+
``agent_venv`` is mandatory on every machine, so it doesn't use this
|
|
68
|
+
helper.
|
|
69
|
+
"""
|
|
70
|
+
out: dict[str, dict] = {}
|
|
71
|
+
if ctx.fleet is None:
|
|
72
|
+
return out
|
|
73
|
+
for name, entry in ctx.fleet.machines.items():
|
|
74
|
+
checks = (entry or {}).get("checks") or {}
|
|
75
|
+
for r in checks.get("results", []) or []:
|
|
76
|
+
if r.get("check_id") == check_id and not r.get("error"):
|
|
77
|
+
out[name] = r.get("values") or {}
|
|
78
|
+
break
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _agent_lane_versions(ctx: HealthContext) -> dict[str, str | None]:
|
|
83
|
+
"""machine_name -> its reported ``agent_venv`` version, or ``None``.
|
|
84
|
+
|
|
85
|
+
``None`` covers both "machine offline" and "machine online but its
|
|
86
|
+
``agent_venv`` check didn't run/errored" — both are "no data", not
|
|
87
|
+
"matches everyone else". Unlike ``_machine_check_values``, every machine
|
|
88
|
+
in ``ctx.fleet.machines`` gets an entry — ``agent_venv`` is mandatory,
|
|
89
|
+
so a machine with nothing to say about it is a missing lane, not an
|
|
90
|
+
inapplicable one.
|
|
91
|
+
"""
|
|
92
|
+
out: dict[str, str | None] = {}
|
|
93
|
+
if ctx.fleet is None:
|
|
94
|
+
return out
|
|
95
|
+
for name, entry in ctx.fleet.machines.items():
|
|
96
|
+
checks = (entry or {}).get("checks") or {}
|
|
97
|
+
version = None
|
|
98
|
+
for r in checks.get("results", []) or []:
|
|
99
|
+
if r.get("check_id") == "agent_venv" and not r.get("error"):
|
|
100
|
+
version = (r.get("values") or {}).get("version") or None
|
|
101
|
+
break
|
|
102
|
+
out[name] = version
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _machine_check_rows(ctx: HealthContext, check_id: str) -> dict[str, list[dict]]:
|
|
107
|
+
"""machine_name -> every non-errored result row this machine reported for
|
|
108
|
+
*check_id*.
|
|
109
|
+
|
|
110
|
+
:func:`_machine_check_values` stops at the first match, which is right for
|
|
111
|
+
the singleton checks it was written for. ``spawned_coord`` (#1834) reports
|
|
112
|
+
one row **per running unit**, so a first-match read would see only
|
|
113
|
+
``coord-agent`` and structurally miss ``coord-serve`` — the one unit whose
|
|
114
|
+
spawned version was the whole 2026-08-04 incident.
|
|
115
|
+
"""
|
|
116
|
+
out: dict[str, list[dict]] = {}
|
|
117
|
+
if ctx.fleet is None:
|
|
118
|
+
return out
|
|
119
|
+
for name, entry in ctx.fleet.machines.items():
|
|
120
|
+
checks = (entry or {}).get("checks") or {}
|
|
121
|
+
rows = [
|
|
122
|
+
r
|
|
123
|
+
for r in (checks.get("results") or [])
|
|
124
|
+
if r.get("check_id") == check_id and not r.get("error")
|
|
125
|
+
]
|
|
126
|
+
if rows:
|
|
127
|
+
out[name] = rows
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _spawned_lanes(ctx: HealthContext) -> dict[str, str]:
|
|
132
|
+
"""``<unit> spawns (<machine>)`` -> the version that unit would actually
|
|
133
|
+
spawn, for every running coord service across the fleet (#1834).
|
|
134
|
+
|
|
135
|
+
This is the lane that did not exist on 2026-08-04, and its absence is why
|
|
136
|
+
every other lane in this check read green while the fleet was running two
|
|
137
|
+
versions: each of them measures an *install*, and the defect lived in what
|
|
138
|
+
``shutil.which("coord")`` resolved to inside a live service's PATH.
|
|
139
|
+
See :mod:`coord.health.checks.spawned_coord`.
|
|
140
|
+
|
|
141
|
+
Only units with a resolvable spawned version become lanes. A unit whose
|
|
142
|
+
PATH has no ``coord`` on it at all is deliberately **not** a lane: its
|
|
143
|
+
subprocesses fall back to ``python -m coord.cli`` on the parent's own
|
|
144
|
+
interpreter, which cannot disagree with the parent, so admitting it as a
|
|
145
|
+
null lane would manufacture a permanent "missing lane" UNKNOWN on every
|
|
146
|
+
correctly-deployed fleet.
|
|
147
|
+
"""
|
|
148
|
+
out: dict[str, str] = {}
|
|
149
|
+
for machine, rows in _machine_check_rows(ctx, "spawned_coord").items():
|
|
150
|
+
for row in rows:
|
|
151
|
+
values = row.get("values") or {}
|
|
152
|
+
version = values.get("version")
|
|
153
|
+
unit = row.get("subject") or values.get("unit")
|
|
154
|
+
if version and unit:
|
|
155
|
+
out[f"{unit} spawns ({machine})"] = version
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cli_venv_lanes(ctx: HealthContext) -> dict[str, str | None]:
|
|
160
|
+
"""``~/.coord-cli-venv (<machine>)`` -> version, for every machine whose
|
|
161
|
+
own ``cli_venv`` check (#1806) reports one present.
|
|
162
|
+
|
|
163
|
+
Named per-machine, unlike the daemon-host fact it replaces, because more
|
|
164
|
+
than one machine can plausibly have a CLI venv (more than one operator).
|
|
165
|
+
When *no* machine reports one present, a single ``"~/.coord-cli-venv":
|
|
166
|
+
None`` entry stands in — "no data anywhere", not "matches everyone else"
|
|
167
|
+
just because nothing disagreed.
|
|
168
|
+
"""
|
|
169
|
+
out: dict[str, str | None] = {}
|
|
170
|
+
for name, values in _machine_check_values(ctx, "cli_venv").items():
|
|
171
|
+
if values.get("present"):
|
|
172
|
+
out[f"~/.coord-cli-venv ({name})"] = values.get("version") or None
|
|
173
|
+
if not out:
|
|
174
|
+
out["~/.coord-cli-venv"] = None
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@check(
|
|
179
|
+
id="fleet_deploy_lanes",
|
|
180
|
+
scope="fleet",
|
|
181
|
+
title="deploy lanes",
|
|
182
|
+
order=10,
|
|
183
|
+
description=(
|
|
184
|
+
"Every ~/.coord-venv (per agent), the daemon's own coord-serve "
|
|
185
|
+
"install, ~/.coord-cli-venv, and the coord each running service "
|
|
186
|
+
"would actually spawn all report the same coordinator version."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
def probe_deploy_lanes(ctx: HealthContext) -> CheckResult:
|
|
190
|
+
if ctx.fleet is None:
|
|
191
|
+
return CheckResult(
|
|
192
|
+
check_id="fleet_deploy_lanes",
|
|
193
|
+
scope="fleet",
|
|
194
|
+
severity=Severity.UNKNOWN,
|
|
195
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
lanes: dict[str, str | None] = dict(_agent_lane_versions(ctx))
|
|
199
|
+
dh = ctx.fleet.daemon_host or {}
|
|
200
|
+
lanes["coord-serve (daemon host)"] = dh.get("coord_serve_version")
|
|
201
|
+
lanes.update(_cli_venv_lanes(ctx))
|
|
202
|
+
# #1834: what each running service would actually SPAWN. Added last and
|
|
203
|
+
# only where known, so it can introduce skew but never a missing lane.
|
|
204
|
+
lanes.update(_spawned_lanes(ctx))
|
|
205
|
+
|
|
206
|
+
known = {v for v in lanes.values() if v}
|
|
207
|
+
missing = sorted(name for name, v in lanes.items() if not v)
|
|
208
|
+
|
|
209
|
+
if not known:
|
|
210
|
+
return CheckResult(
|
|
211
|
+
check_id="fleet_deploy_lanes",
|
|
212
|
+
scope="fleet",
|
|
213
|
+
severity=Severity.UNKNOWN,
|
|
214
|
+
headroom="no lane has a resolvable version yet",
|
|
215
|
+
detail=f"no data for: {', '.join(missing)}" if missing else "",
|
|
216
|
+
values={"lanes": lanes},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if len(known) > 1:
|
|
220
|
+
by_version: dict[str, list[str]] = {}
|
|
221
|
+
for name, v in lanes.items():
|
|
222
|
+
if v:
|
|
223
|
+
by_version.setdefault(v, []).append(name)
|
|
224
|
+
skew_desc = "; ".join(
|
|
225
|
+
f"{v}: {', '.join(sorted(names))}" for v, names in sorted(by_version.items())
|
|
226
|
+
)
|
|
227
|
+
return CheckResult(
|
|
228
|
+
check_id="fleet_deploy_lanes",
|
|
229
|
+
scope="fleet",
|
|
230
|
+
severity=Severity.CRIT,
|
|
231
|
+
headroom=f"{len(known)} versions live across the fleet",
|
|
232
|
+
detail=skew_desc,
|
|
233
|
+
threshold="crit when any lane disagrees",
|
|
234
|
+
values={"lanes": lanes},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
(version,) = known
|
|
238
|
+
headroom = f"all lanes on {version}"
|
|
239
|
+
if missing:
|
|
240
|
+
headroom += f" ({len(missing)} lane(s) with no data)"
|
|
241
|
+
return CheckResult(
|
|
242
|
+
check_id="fleet_deploy_lanes",
|
|
243
|
+
scope="fleet",
|
|
244
|
+
severity=Severity.OK if not missing else Severity.UNKNOWN,
|
|
245
|
+
headroom=headroom,
|
|
246
|
+
detail=f"no data for: {', '.join(missing)}" if missing else "",
|
|
247
|
+
values={"lanes": lanes},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@check(
|
|
252
|
+
id="fleet_tui_binary",
|
|
253
|
+
scope="fleet",
|
|
254
|
+
title="tui binary",
|
|
255
|
+
order=11,
|
|
256
|
+
description=(
|
|
257
|
+
"The locally-built tui/ binary is not older than the tui/ source "
|
|
258
|
+
"tree it was supposedly built from, on every machine that has one."
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
def probe_tui_binary(ctx: HealthContext) -> CheckResult:
|
|
262
|
+
"""Aggregates every machine's own ``tui_binary`` machine-scope check
|
|
263
|
+
(:mod:`coord.health.checks.deploy_lane_facts`, #1806) instead of
|
|
264
|
+
``os.stat``-ing a single path on the daemon host — the daemon host is
|
|
265
|
+
frequently not the operator's machine, so a single-path check there was
|
|
266
|
+
structurally blind to the binary that actually matters.
|
|
267
|
+
"""
|
|
268
|
+
if ctx.fleet is None:
|
|
269
|
+
return CheckResult(
|
|
270
|
+
check_id="fleet_tui_binary",
|
|
271
|
+
scope="fleet",
|
|
272
|
+
severity=Severity.UNKNOWN,
|
|
273
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
facts = _machine_check_values(ctx, "tui_binary")
|
|
277
|
+
present = {name: v for name, v in facts.items() if v.get("present")}
|
|
278
|
+
|
|
279
|
+
if not present:
|
|
280
|
+
return CheckResult(
|
|
281
|
+
check_id="fleet_tui_binary",
|
|
282
|
+
scope="fleet",
|
|
283
|
+
severity=Severity.UNKNOWN,
|
|
284
|
+
headroom="no machine reports a coord-tui binary",
|
|
285
|
+
detail=(
|
|
286
|
+
"build and install it (`cd tui && cargo build && cp "
|
|
287
|
+
"target/debug/coord-tui ~/.local/bin/coord-tui`) on the "
|
|
288
|
+
"machine you actually run the tui from, or set "
|
|
289
|
+
"health.tui_binary_path if it lives elsewhere"
|
|
290
|
+
),
|
|
291
|
+
values={"machines": facts},
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Compare each machine against ITS OWN source tree — a machine with no
|
|
295
|
+
# tui/ checkout has nothing to compare against and is left out of the
|
|
296
|
+
# verdict (present, uncomparable), same as the single-machine case
|
|
297
|
+
# always was; it just no longer speaks for the whole fleet.
|
|
298
|
+
stale: list[tuple[str, float, float]] = []
|
|
299
|
+
comparable: list[str] = []
|
|
300
|
+
for name, values in present.items():
|
|
301
|
+
binary_mtime = values.get("binary_mtime")
|
|
302
|
+
source_mtime = values.get("source_mtime")
|
|
303
|
+
if binary_mtime is None or source_mtime is None:
|
|
304
|
+
continue
|
|
305
|
+
comparable.append(name)
|
|
306
|
+
if source_mtime > binary_mtime:
|
|
307
|
+
stale.append((name, binary_mtime, source_mtime))
|
|
308
|
+
|
|
309
|
+
if stale:
|
|
310
|
+
# Worst (most hours stale) first, but name every stale machine —
|
|
311
|
+
# this check exists specifically so a stale lane is never silent.
|
|
312
|
+
stale.sort(key=lambda t: (t[2] - t[1]), reverse=True)
|
|
313
|
+
names = ", ".join(n for n, _, _ in stale)
|
|
314
|
+
_worst_name, worst_bm, worst_sm = stale[0]
|
|
315
|
+
stale_hours = (worst_sm - worst_bm) / 3600.0
|
|
316
|
+
return CheckResult(
|
|
317
|
+
check_id="fleet_tui_binary",
|
|
318
|
+
scope="fleet",
|
|
319
|
+
severity=Severity.WARN,
|
|
320
|
+
headroom=f"{names}: binary is {stale_hours:.1f}h older than tui/ source",
|
|
321
|
+
detail=f"rebuild the tui/ binary on: {names}",
|
|
322
|
+
threshold="warn when any machine's source/ is newer than its built binary",
|
|
323
|
+
values={"machines": facts, "stale": [n for n, _, _ in stale]},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if not comparable:
|
|
327
|
+
return CheckResult(
|
|
328
|
+
check_id="fleet_tui_binary",
|
|
329
|
+
scope="fleet",
|
|
330
|
+
severity=Severity.OK,
|
|
331
|
+
headroom=(
|
|
332
|
+
f"binary present on {', '.join(sorted(present))} "
|
|
333
|
+
"(no source tree found to compare)"
|
|
334
|
+
),
|
|
335
|
+
values={"machines": facts},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return CheckResult(
|
|
339
|
+
check_id="fleet_tui_binary",
|
|
340
|
+
scope="fleet",
|
|
341
|
+
severity=Severity.OK,
|
|
342
|
+
headroom=f"up to date with tui/ source on {', '.join(sorted(comparable))}",
|
|
343
|
+
values={"machines": facts},
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@check(
|
|
348
|
+
id="fleet_webapp_bundle",
|
|
349
|
+
scope="fleet",
|
|
350
|
+
title="webapp bundle",
|
|
351
|
+
order=13,
|
|
352
|
+
description=(
|
|
353
|
+
"The dist/ bundle `coord web --dist` serves is not older than the "
|
|
354
|
+
"coord/dashboard/webapp/ source tree it was supposedly built from, "
|
|
355
|
+
"on every machine that has one (#1834 lane 5)."
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
def probe_webapp_bundle(ctx: HealthContext) -> CheckResult:
|
|
359
|
+
"""Aggregates every machine's own ``webapp_bundle`` machine-scope check
|
|
360
|
+
— the same "measure locally, judge centrally" split :func:`probe_tui_binary`
|
|
361
|
+
uses above, for the same reason: the machine actually running `coord web`
|
|
362
|
+
is very often not the daemon host.
|
|
363
|
+
|
|
364
|
+
Deliberately NOT compared against the released wheel's version, unlike
|
|
365
|
+
every other lane in this module: `coord-web-dist-build.timer` publishes
|
|
366
|
+
continuously off `origin/main`'s SHA (#1543), decoupled on purpose from
|
|
367
|
+
the ~/.coord-venv release cadence, so "is it at the released version?" is
|
|
368
|
+
not a well-formed question for it — see docs/AGENT_OPERATIONS.md's
|
|
369
|
+
`coord release verify` section. What IS well-formed, and what this
|
|
370
|
+
checks: whether the live bundle is stale relative to the source it
|
|
371
|
+
claims to have been built from, and whether machines that both run
|
|
372
|
+
`coord web` agree on which bundle that is.
|
|
373
|
+
"""
|
|
374
|
+
if ctx.fleet is None:
|
|
375
|
+
return CheckResult(
|
|
376
|
+
check_id="fleet_webapp_bundle",
|
|
377
|
+
scope="fleet",
|
|
378
|
+
severity=Severity.UNKNOWN,
|
|
379
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
facts = _machine_check_values(ctx, "webapp_bundle")
|
|
383
|
+
present = {name: v for name, v in facts.items() if v.get("present")}
|
|
384
|
+
|
|
385
|
+
if not present:
|
|
386
|
+
return CheckResult(
|
|
387
|
+
check_id="fleet_webapp_bundle",
|
|
388
|
+
scope="fleet",
|
|
389
|
+
severity=Severity.UNKNOWN,
|
|
390
|
+
headroom="no machine reports a coord-web-dist bundle",
|
|
391
|
+
detail=(
|
|
392
|
+
"install deploy/coord-web-dist-build.service + .timer on the "
|
|
393
|
+
"machine that runs `coord web` (see docs/AGENT_OPERATIONS.md), "
|
|
394
|
+
"or set health.webapp_dist_path if it lives elsewhere"
|
|
395
|
+
),
|
|
396
|
+
values={"machines": facts},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Compare each machine against ITS OWN source tree, same reasoning as
|
|
400
|
+
# fleet_tui_binary: a machine with no webapp/ checkout has nothing to
|
|
401
|
+
# compare against and is left out of the verdict (present, uncomparable).
|
|
402
|
+
stale: list[tuple[str, float, float]] = []
|
|
403
|
+
comparable: list[str] = []
|
|
404
|
+
for name, values in present.items():
|
|
405
|
+
dist_mtime = values.get("dist_mtime")
|
|
406
|
+
source_mtime = values.get("source_mtime")
|
|
407
|
+
if dist_mtime is None or source_mtime is None:
|
|
408
|
+
continue
|
|
409
|
+
comparable.append(name)
|
|
410
|
+
if source_mtime > dist_mtime:
|
|
411
|
+
stale.append((name, dist_mtime, source_mtime))
|
|
412
|
+
|
|
413
|
+
if stale:
|
|
414
|
+
stale.sort(key=lambda t: (t[2] - t[1]), reverse=True)
|
|
415
|
+
names = ", ".join(n for n, _, _ in stale)
|
|
416
|
+
_worst_name, worst_dm, worst_sm = stale[0]
|
|
417
|
+
stale_hours = (worst_sm - worst_dm) / 3600.0
|
|
418
|
+
return CheckResult(
|
|
419
|
+
check_id="fleet_webapp_bundle",
|
|
420
|
+
scope="fleet",
|
|
421
|
+
severity=Severity.WARN,
|
|
422
|
+
headroom=f"{names}: bundle is {stale_hours:.1f}h older than webapp/ source",
|
|
423
|
+
detail=f"check coord-web-dist-build.timer on: {names}",
|
|
424
|
+
threshold="warn when any machine's webapp/ source is newer than its live bundle",
|
|
425
|
+
values={"machines": facts, "stale": [n for n, _, _ in stale]},
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Two machines both running `coord web` but serving different builds is
|
|
429
|
+
# its own drift, independent of staleness against source — most fleets
|
|
430
|
+
# only ever have one, but when there are two they must agree.
|
|
431
|
+
shas = {v.get("sha") for v in present.values() if v.get("sha")}
|
|
432
|
+
if len(shas) > 1:
|
|
433
|
+
by_sha: dict[str, list[str]] = {}
|
|
434
|
+
for name, v in present.items():
|
|
435
|
+
sha = v.get("sha")
|
|
436
|
+
if sha:
|
|
437
|
+
by_sha.setdefault(sha, []).append(name)
|
|
438
|
+
skew_desc = "; ".join(
|
|
439
|
+
f"{sha}: {', '.join(sorted(names))}" for sha, names in sorted(by_sha.items())
|
|
440
|
+
)
|
|
441
|
+
return CheckResult(
|
|
442
|
+
check_id="fleet_webapp_bundle",
|
|
443
|
+
scope="fleet",
|
|
444
|
+
severity=Severity.WARN,
|
|
445
|
+
headroom=f"{len(shas)} different bundles live across the fleet",
|
|
446
|
+
detail=skew_desc,
|
|
447
|
+
threshold="warn when machines serving coord web disagree on the live bundle",
|
|
448
|
+
values={"machines": facts},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if not comparable:
|
|
452
|
+
return CheckResult(
|
|
453
|
+
check_id="fleet_webapp_bundle",
|
|
454
|
+
scope="fleet",
|
|
455
|
+
severity=Severity.OK,
|
|
456
|
+
headroom=(
|
|
457
|
+
f"bundle present on {', '.join(sorted(present))} "
|
|
458
|
+
"(no source tree found to compare)"
|
|
459
|
+
),
|
|
460
|
+
values={"machines": facts},
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return CheckResult(
|
|
464
|
+
check_id="fleet_webapp_bundle",
|
|
465
|
+
scope="fleet",
|
|
466
|
+
severity=Severity.OK,
|
|
467
|
+
headroom=f"up to date with webapp/ source on {', '.join(sorted(comparable))}",
|
|
468
|
+
values={"machines": facts},
|
|
469
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Fleet-scope: phantom ``running`` board rows (#1630).
|
|
2
|
+
|
|
3
|
+
A phantom ``running`` row (board says running, the owning agent no longer
|
|
4
|
+
agrees) silently disables ``coord retry`` fleet-wide — see
|
|
5
|
+
``coord.reconcile.describe_no_candidate_machines`` and ``coord/diagnose.py``
|
|
6
|
+
for the interactive/manual side of this failure mode. This check is the
|
|
7
|
+
passive, always-on fleet-health signal for the same class of row: the
|
|
8
|
+
daemon's health-poll tick cross-references board ``running`` rows against
|
|
9
|
+
each machine's own ``/status`` and hands this probe the count it found — read
|
|
10
|
+
only, never a write, never a finalize (that stays ``coord diagnose``'s job).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
16
|
+
from coord.health.registry import check
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@check(
|
|
20
|
+
id="fleet_phantom_running",
|
|
21
|
+
scope="fleet",
|
|
22
|
+
title="phantom running rows",
|
|
23
|
+
order=21,
|
|
24
|
+
description=(
|
|
25
|
+
"Board rows marked running whose owning agent no longer reports "
|
|
26
|
+
"them as running — known to silently disable `coord retry`."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
def probe_phantom_running(ctx: HealthContext) -> CheckResult:
|
|
30
|
+
if ctx.fleet is None:
|
|
31
|
+
return CheckResult(
|
|
32
|
+
check_id="fleet_phantom_running",
|
|
33
|
+
scope="fleet",
|
|
34
|
+
severity=Severity.UNKNOWN,
|
|
35
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
dh = ctx.fleet.daemon_host or {}
|
|
39
|
+
phantom = dh.get("phantom_running")
|
|
40
|
+
if phantom is None:
|
|
41
|
+
return CheckResult(
|
|
42
|
+
check_id="fleet_phantom_running",
|
|
43
|
+
scope="fleet",
|
|
44
|
+
severity=Severity.UNKNOWN,
|
|
45
|
+
headroom="no phantom-row scan yet",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
count = len(phantom)
|
|
49
|
+
if count == 0:
|
|
50
|
+
return CheckResult(
|
|
51
|
+
check_id="fleet_phantom_running",
|
|
52
|
+
scope="fleet",
|
|
53
|
+
severity=Severity.OK,
|
|
54
|
+
headroom="0 phantom running rows",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
sample = ", ".join(
|
|
58
|
+
f"{p.get('repo_name')}#{p.get('issue_number')}@{p.get('machine')}"
|
|
59
|
+
for p in phantom[:5]
|
|
60
|
+
)
|
|
61
|
+
return CheckResult(
|
|
62
|
+
check_id="fleet_phantom_running",
|
|
63
|
+
scope="fleet",
|
|
64
|
+
severity=Severity.CRIT,
|
|
65
|
+
headroom=f"{count} phantom running row{'s' if count != 1 else ''}",
|
|
66
|
+
detail=f"e.g. {sample}" + (", ..." if count > 5 else ""),
|
|
67
|
+
threshold="crit when any phantom row is found",
|
|
68
|
+
values={"count": count, "assignment_ids": [p.get("assignment_id") for p in phantom]},
|
|
69
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Fleet-scope: does any machine's installed unit drift from the released
|
|
2
|
+
unit files? (#1831, #1927)
|
|
3
|
+
|
|
4
|
+
Aggregates every machine's own `unit_drift` machine-scope check
|
|
5
|
+
(:mod:`coord.health.checks.unit_drift`) instead of reading the filesystem
|
|
6
|
+
itself — a unit file is inherently a fact about whichever machine it's
|
|
7
|
+
installed on, exactly like `cli_venv`/`tui_binary`
|
|
8
|
+
(:mod:`coord.health.checks.deploy_lane_facts`) and the fleet aggregator that
|
|
9
|
+
reads them (:mod:`coord.health.checks.fleet_deploy_lanes`).
|
|
10
|
+
|
|
11
|
+
Fail-soft toward UNKNOWN, never toward OK: a fleet with no machine yet
|
|
12
|
+
reporting `unit_drift` (every agent still predates #1831) must not read as
|
|
13
|
+
"nothing wrong" just because nothing has disagreed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
19
|
+
from coord.health.registry import check
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _all_unit_drift_results(ctx: HealthContext) -> list[tuple[str, dict]]:
|
|
23
|
+
"""[(machine_name, result_dict), ...] for every reported `unit_drift`
|
|
24
|
+
result across the fleet — one entry per (machine, unit), since the
|
|
25
|
+
machine-scope check returns one result per deploy-lane unit."""
|
|
26
|
+
out: list[tuple[str, dict]] = []
|
|
27
|
+
if ctx.fleet is None:
|
|
28
|
+
return out
|
|
29
|
+
for name, entry in ctx.fleet.machines.items():
|
|
30
|
+
checks = (entry or {}).get("checks") or {}
|
|
31
|
+
for r in checks.get("results", []) or []:
|
|
32
|
+
if r.get("check_id") == "unit_drift" and not r.get("error"):
|
|
33
|
+
out.append((name, r))
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@check(
|
|
38
|
+
id="fleet_unit_drift",
|
|
39
|
+
scope="fleet",
|
|
40
|
+
title="unit drift",
|
|
41
|
+
order=12,
|
|
42
|
+
description=(
|
|
43
|
+
"Every machine's installed systemd units match deploy/, and none "
|
|
44
|
+
"shadow the release with an editable checkout on PATH (#1831)."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
def probe_fleet_unit_drift(ctx: HealthContext) -> CheckResult:
|
|
48
|
+
if ctx.fleet is None:
|
|
49
|
+
return CheckResult(
|
|
50
|
+
check_id="fleet_unit_drift",
|
|
51
|
+
scope="fleet",
|
|
52
|
+
severity=Severity.UNKNOWN,
|
|
53
|
+
headroom="no fleet snapshot (fleet checks only run on the daemon)",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
all_results = _all_unit_drift_results(ctx)
|
|
57
|
+
if not all_results:
|
|
58
|
+
return CheckResult(
|
|
59
|
+
check_id="fleet_unit_drift",
|
|
60
|
+
scope="fleet",
|
|
61
|
+
severity=Severity.UNKNOWN,
|
|
62
|
+
headroom="no machine has reported unit_drift data yet",
|
|
63
|
+
detail="every agent predates #1831, or unit_drift is disabled",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
shadowed = [
|
|
67
|
+
(m, r) for m, r in all_results if r.get("severity") == "crit"
|
|
68
|
+
]
|
|
69
|
+
stale = [(m, r) for m, r in all_results if r.get("severity") == "warn"]
|
|
70
|
+
checked = [(m, r) for m, r in all_results if (r.get("values") or {}).get("installed")]
|
|
71
|
+
# #1927: a machine that diffed against its own git working copy proved
|
|
72
|
+
# nothing — a stale checkout and a stale installed unit agree with each
|
|
73
|
+
# other. Those results arrive as UNKNOWN and must not be counted toward
|
|
74
|
+
# the fleet's green.
|
|
75
|
+
unverified = [
|
|
76
|
+
(m, r)
|
|
77
|
+
for m, r in all_results
|
|
78
|
+
if (r.get("values") or {}).get("installed")
|
|
79
|
+
and (r.get("values") or {}).get("reference_verified") is False
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
values = {
|
|
83
|
+
"machines": sorted({m for m, _ in all_results}),
|
|
84
|
+
"checked_units": len(checked),
|
|
85
|
+
"shadowed": [
|
|
86
|
+
{"machine": m, "unit": r.get("subject")} for m, r in shadowed
|
|
87
|
+
],
|
|
88
|
+
"stale": [{"machine": m, "unit": r.get("subject")} for m, r in stale],
|
|
89
|
+
"unverified_reference": [
|
|
90
|
+
{"machine": m, "unit": r.get("subject")} for m, r in unverified
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if shadowed:
|
|
95
|
+
names = ", ".join(f"{m}/{r.get('subject')}" for m, r in shadowed)
|
|
96
|
+
return CheckResult(
|
|
97
|
+
check_id="fleet_unit_drift",
|
|
98
|
+
scope="fleet",
|
|
99
|
+
severity=Severity.CRIT,
|
|
100
|
+
headroom=f"editable checkout shadows the release on PATH: {names}",
|
|
101
|
+
detail=(
|
|
102
|
+
"shutil.which(\"coord\") in a unit's subprocesses resolves a "
|
|
103
|
+
"stale checkout instead of the pinned release — see each "
|
|
104
|
+
"machine's own unit_drift check for the fix"
|
|
105
|
+
),
|
|
106
|
+
threshold="crit when any unit's PATH lets a .venv/bin entry precede the release",
|
|
107
|
+
values=values,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if stale:
|
|
111
|
+
names = ", ".join(f"{m}/{r.get('subject')}" for m, r in stale)
|
|
112
|
+
return CheckResult(
|
|
113
|
+
check_id="fleet_unit_drift",
|
|
114
|
+
scope="fleet",
|
|
115
|
+
severity=Severity.WARN,
|
|
116
|
+
headroom=f"installed unit(s) stale vs the released units: {names}",
|
|
117
|
+
detail="cp the reference unit over the installed one and restart it — see the machine-scope unit_drift detail for the exact command",
|
|
118
|
+
threshold="warn when any installed unit's content != the released unit",
|
|
119
|
+
values=values,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if unverified:
|
|
123
|
+
names = ", ".join(
|
|
124
|
+
sorted({f"{m}/{r.get('subject')}" for m, r in unverified})
|
|
125
|
+
)
|
|
126
|
+
return CheckResult(
|
|
127
|
+
check_id="fleet_unit_drift",
|
|
128
|
+
scope="fleet",
|
|
129
|
+
severity=Severity.UNKNOWN,
|
|
130
|
+
headroom=f"reference is an unverified working copy on: {names}",
|
|
131
|
+
detail=(
|
|
132
|
+
"these machines diffed their installed units against a git "
|
|
133
|
+
"checkout nothing verifies is current, so a match proves "
|
|
134
|
+
"nothing — both sides go stale together (#1927). Install a "
|
|
135
|
+
"release wheel there; it ships the reference units."
|
|
136
|
+
),
|
|
137
|
+
values=values,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return CheckResult(
|
|
141
|
+
check_id="fleet_unit_drift",
|
|
142
|
+
scope="fleet",
|
|
143
|
+
severity=Severity.OK,
|
|
144
|
+
headroom=(
|
|
145
|
+
f"{len(checked)} installed unit(s) across {len(values['machines'])} "
|
|
146
|
+
"machine(s) match the released units"
|
|
147
|
+
if checked
|
|
148
|
+
else "no machine has any deploy-lane unit installed"
|
|
149
|
+
),
|
|
150
|
+
values=values,
|
|
151
|
+
)
|