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,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
|
+
)
|