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,192 @@
|
|
|
1
|
+
"""graphify graph freshness per checkout (#1628).
|
|
2
|
+
|
|
3
|
+
**This is a wrapper, not a reimplementation.** ``coord.graph_health``
|
|
4
|
+
already knows how to answer both halves — :func:`~coord.graph_health.graph_status`
|
|
5
|
+
compares ``GRAPH_REPORT.md``'s "Built from commit" against HEAD (with the
|
|
6
|
+
``manifest.json``-mtime escape hatch that stops a genuinely-current graph
|
|
7
|
+
from reporting STALE forever), and
|
|
8
|
+
:func:`~coord.graph_health.hooks_path_status` checks the ``core.hooksPath``
|
|
9
|
+
setting that decides whether anything will ever rebuild it. ``coord
|
|
10
|
+
diagnose --graph`` renders them today. Forking either would guarantee the
|
|
11
|
+
two surfaces drift apart, which is the exact failure this milestone's
|
|
12
|
+
"renderers must never re-derive severity" rule exists to prevent — so this
|
|
13
|
+
module calls them and maps their output to a severity.
|
|
14
|
+
|
|
15
|
+
**Why hooks-disabled makes a stale graph CRIT rather than WARN.** A stale
|
|
16
|
+
graph with working hooks is a nuisance: the next commit rebuilds it. A
|
|
17
|
+
stale graph on a checkout with ``core.hooksPath`` unset (or with hooks
|
|
18
|
+
orphaned into ``.git/hooks``, which git ignores entirely once
|
|
19
|
+
``core.hooksPath`` is set) **will not self-heal at any point in the future**
|
|
20
|
+
— it stays wrong until a human runs ``graphify update`` by hand, and every
|
|
21
|
+
agent that queries it in the meantime gets answers about a commit that is no
|
|
22
|
+
longer HEAD. That is the 2026-07-30 vimcode incident: 128.8h stale, hooks
|
|
23
|
+
disabled, and nothing in the fleet said so. Age escalates a
|
|
24
|
+
hooks-working checkout from WARN to CRIT at ``graph_stale_crit_hours``;
|
|
25
|
+
hooks-disabled skips straight to CRIT because time cannot fix it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import subprocess
|
|
31
|
+
|
|
32
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
33
|
+
from coord.health.registry import check
|
|
34
|
+
from coord.health.units import human_hours
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _commits_behind(repo_path: str, built_sha: str, head_sha: str) -> int | None:
|
|
38
|
+
"""How many commits separate ``built_sha`` from ``head_sha``.
|
|
39
|
+
|
|
40
|
+
Purely informational — an operator (and #1728's H-6 successor, deciding
|
|
41
|
+
how urgently to rebuild) reads better from "13 commits behind" than from
|
|
42
|
+
an age in hours, which conflates "how stale" with "how busy was the
|
|
43
|
+
repo". This does NOT feed severity: :func:`~coord.graph_health.graph_status`
|
|
44
|
+
already decided ``stale`` from the SHA comparison plus the
|
|
45
|
+
manifest-mtime escape hatch, and duplicating that decision here from a
|
|
46
|
+
commit count would be exactly the second copy this module's docstring
|
|
47
|
+
warns against. Best-effort: returns ``None`` (never raises) when the
|
|
48
|
+
shas are abbreviated past what git can resolve, the repo can't be read,
|
|
49
|
+
or the call times out.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
r = subprocess.run(
|
|
53
|
+
["git", "-C", str(repo_path), "rev-list", "--count", f"{built_sha}..{head_sha}"],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=10.0,
|
|
57
|
+
)
|
|
58
|
+
except (subprocess.SubprocessError, OSError):
|
|
59
|
+
return None
|
|
60
|
+
if r.returncode != 0:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return int(r.stdout.strip())
|
|
64
|
+
except ValueError:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@check(
|
|
69
|
+
id="graph",
|
|
70
|
+
scope="checkout",
|
|
71
|
+
title="graph",
|
|
72
|
+
order=70,
|
|
73
|
+
description="graphify graph freshness vs HEAD, and whether hooks can heal it.",
|
|
74
|
+
)
|
|
75
|
+
def probe_graph(ctx: HealthContext) -> list[CheckResult]:
|
|
76
|
+
from coord.graph_health import graph_status, hooks_path_status # noqa: PLC0415
|
|
77
|
+
|
|
78
|
+
th = ctx.thresholds
|
|
79
|
+
results: list[CheckResult] = []
|
|
80
|
+
|
|
81
|
+
for checkout in ctx.checkouts:
|
|
82
|
+
status = graph_status(checkout.path)
|
|
83
|
+
hooks_ok, hooks_detail = hooks_path_status(checkout.path)
|
|
84
|
+
|
|
85
|
+
values = {
|
|
86
|
+
"path": str(checkout.path),
|
|
87
|
+
"present": status.present,
|
|
88
|
+
"in_sync": status.in_sync,
|
|
89
|
+
"stale": status.stale,
|
|
90
|
+
"stamp_behind": status.stamp_behind,
|
|
91
|
+
"verified_current": status.verified_current,
|
|
92
|
+
"built_sha": status.built_sha,
|
|
93
|
+
"head_sha": status.head_sha,
|
|
94
|
+
"age_seconds": status.age_seconds,
|
|
95
|
+
"age_hours": (
|
|
96
|
+
round(status.age_seconds / 3600.0, 1) if status.age_seconds is not None else None
|
|
97
|
+
),
|
|
98
|
+
"is_symlink": status.is_symlink,
|
|
99
|
+
"hooks_ok": hooks_ok,
|
|
100
|
+
"hooks_detail": hooks_detail,
|
|
101
|
+
"unknown_reason": status.unknown_reason,
|
|
102
|
+
"warn_hours": th.graph_stale_warn_hours,
|
|
103
|
+
"crit_hours": th.graph_stale_crit_hours,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if not status.present:
|
|
107
|
+
# No graph at all. Agents are told to query it first (CLAUDE.md),
|
|
108
|
+
# so its absence silently downgrades every one of them to grep.
|
|
109
|
+
results.append(
|
|
110
|
+
CheckResult(
|
|
111
|
+
check_id="graph",
|
|
112
|
+
scope="checkout",
|
|
113
|
+
subject=checkout.name,
|
|
114
|
+
severity=Severity.WARN,
|
|
115
|
+
headroom="no graph built here",
|
|
116
|
+
detail=status.unknown_reason or "",
|
|
117
|
+
threshold="warn when absent",
|
|
118
|
+
values=values,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if status.unknown_reason and not status.stamp_behind:
|
|
124
|
+
results.append(
|
|
125
|
+
CheckResult(
|
|
126
|
+
check_id="graph",
|
|
127
|
+
scope="checkout",
|
|
128
|
+
subject=checkout.name,
|
|
129
|
+
severity=Severity.UNKNOWN,
|
|
130
|
+
headroom=f"freshness unknown — {status.unknown_reason}",
|
|
131
|
+
error=status.unknown_reason,
|
|
132
|
+
values=values,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
age_hours = (status.age_seconds or 0.0) / 3600.0
|
|
138
|
+
age_text = human_hours(status.age_seconds) if status.age_seconds is not None else "?h"
|
|
139
|
+
|
|
140
|
+
# #1728: how many commits separate the stamp from HEAD. Purely
|
|
141
|
+
# additive to the message — severity above is still `status.stale`
|
|
142
|
+
# (SHA comparison + the manifest-mtime escape hatch), never this
|
|
143
|
+
# count, so a repo that made 1 commit vs. 100 since the graph was
|
|
144
|
+
# built is still judged identically on "is it stale", only described
|
|
145
|
+
# differently.
|
|
146
|
+
commits_behind: int | None = None
|
|
147
|
+
if status.stale and status.built_sha and status.head_sha:
|
|
148
|
+
commits_behind = _commits_behind(checkout.path, status.built_sha, status.head_sha)
|
|
149
|
+
values["commits_behind"] = commits_behind
|
|
150
|
+
commits_suffix = (
|
|
151
|
+
""
|
|
152
|
+
if commits_behind is None
|
|
153
|
+
else f", {commits_behind} commit{'' if commits_behind == 1 else 's'} behind"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not status.stale:
|
|
157
|
+
severity = Severity.OK
|
|
158
|
+
headroom = f"in sync ({(status.built_sha or '')[:8]}), {age_text} old"
|
|
159
|
+
if status.verified_current and status.stamp_behind:
|
|
160
|
+
headroom = f"content current (stamp {(status.built_sha or '')[:8]}), {age_text} old"
|
|
161
|
+
elif not hooks_ok:
|
|
162
|
+
severity = Severity.CRIT
|
|
163
|
+
headroom = f"{age_text} stale{commits_suffix}, hooks disabled -> will not self-heal"
|
|
164
|
+
elif age_hours >= th.graph_stale_crit_hours:
|
|
165
|
+
severity = Severity.CRIT
|
|
166
|
+
headroom = f"{age_text} stale{commits_suffix} (HEAD {(status.head_sha or '')[:8]})"
|
|
167
|
+
elif age_hours >= th.graph_stale_warn_hours:
|
|
168
|
+
severity = Severity.WARN
|
|
169
|
+
headroom = f"{age_text} stale{commits_suffix} (HEAD {(status.head_sha or '')[:8]})"
|
|
170
|
+
else:
|
|
171
|
+
severity = Severity.WARN
|
|
172
|
+
headroom = f"stale, {age_text} old{commits_suffix} (HEAD {(status.head_sha or '')[:8]})"
|
|
173
|
+
|
|
174
|
+
detail = ""
|
|
175
|
+
if severity is not Severity.OK:
|
|
176
|
+
detail = f"fix: graphify update {checkout.path}"
|
|
177
|
+
if not hooks_ok:
|
|
178
|
+
detail = f"{detail} — {hooks_detail}"
|
|
179
|
+
|
|
180
|
+
results.append(
|
|
181
|
+
CheckResult(
|
|
182
|
+
check_id="graph",
|
|
183
|
+
scope="checkout",
|
|
184
|
+
subject=checkout.name,
|
|
185
|
+
severity=severity,
|
|
186
|
+
headroom=headroom,
|
|
187
|
+
detail=detail,
|
|
188
|
+
threshold=f"crit at {th.graph_stale_crit_hours:.0f}h (or any age with hooks off)",
|
|
189
|
+
values=values,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
return results
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Max-plan 5h/weekly window headroom (#1628).
|
|
2
|
+
|
|
3
|
+
Another wrapper, not a rewrite: ``coord.usage_limits`` already probes this
|
|
4
|
+
(``claude -p "/usage" --output-format json``, ~700ms, $0, 0 turns) and
|
|
5
|
+
already caches it for 60s because the endpoint is itself rate-limited and
|
|
6
|
+
Claude Code serves the bars up to an hour stale anyway. Going through
|
|
7
|
+
:func:`~coord.usage_limits.get_plan_limits` means a ``coord health`` run
|
|
8
|
+
next to a ``coord drive`` preflight shares one probe rather than racing it
|
|
9
|
+
into that rate limit.
|
|
10
|
+
|
|
11
|
+
Marked ``cost="network"``: it shells out to ``claude`` and hits Anthropic's
|
|
12
|
+
API, so it is excluded from the cheap set that has to fit the ~2s registry
|
|
13
|
+
budget, and ``coord health --no-network`` skips it entirely (recording that
|
|
14
|
+
it was skipped — "we didn't look" must never render as "nothing wrong").
|
|
15
|
+
|
|
16
|
+
A probe that returns ``status="unknown"`` — no OAuth subscription, an API
|
|
17
|
+
key / Bedrock / Vertex auth mode where the windows simply don't apply, a
|
|
18
|
+
timeout — reports UNKNOWN, never OK. Same rule as the dispatch gate: a read
|
|
19
|
+
we can't trust is not a clean bill of health.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
25
|
+
from coord.health.registry import COST_NETWORK, check
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@check(
|
|
29
|
+
id="plan_usage",
|
|
30
|
+
scope="machine",
|
|
31
|
+
title="plan usage",
|
|
32
|
+
order=80,
|
|
33
|
+
cost=COST_NETWORK,
|
|
34
|
+
description="Headroom left in the account's Max-plan 5h/weekly windows.",
|
|
35
|
+
)
|
|
36
|
+
def probe_plan_usage(ctx: HealthContext) -> CheckResult:
|
|
37
|
+
from coord.usage_limits import get_plan_limits # noqa: PLC0415
|
|
38
|
+
|
|
39
|
+
th = ctx.thresholds
|
|
40
|
+
limits = get_plan_limits()
|
|
41
|
+
|
|
42
|
+
if not limits.ok:
|
|
43
|
+
return CheckResult(
|
|
44
|
+
check_id="plan_usage",
|
|
45
|
+
scope="machine",
|
|
46
|
+
severity=Severity.UNKNOWN,
|
|
47
|
+
headroom="plan windows unavailable (no subscription probe)",
|
|
48
|
+
error=limits.error or "probe returned unknown",
|
|
49
|
+
values=limits.to_dict(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
windows: list[tuple[str, float, str | None]] = []
|
|
53
|
+
if limits.session_pct is not None:
|
|
54
|
+
windows.append(("session", limits.session_pct, limits.session_resets_at))
|
|
55
|
+
if limits.week_pct is not None:
|
|
56
|
+
windows.append(("week", limits.week_pct, limits.week_resets_at))
|
|
57
|
+
|
|
58
|
+
peak = max((pct for _, pct, _ in windows), default=0.0)
|
|
59
|
+
if peak >= th.plan_usage_crit_pct:
|
|
60
|
+
severity = Severity.CRIT
|
|
61
|
+
elif peak >= th.plan_usage_warn_pct:
|
|
62
|
+
severity = Severity.WARN
|
|
63
|
+
else:
|
|
64
|
+
severity = Severity.OK
|
|
65
|
+
|
|
66
|
+
parts = []
|
|
67
|
+
for label, pct, resets in windows:
|
|
68
|
+
reset_note = f" resets {resets}" if resets else ""
|
|
69
|
+
parts.append(f"{label} {pct:.0f}% used{reset_note}")
|
|
70
|
+
headroom = "; ".join(parts) or "no windows reported"
|
|
71
|
+
|
|
72
|
+
values = limits.to_dict()
|
|
73
|
+
values.update(
|
|
74
|
+
{
|
|
75
|
+
"peak_pct": peak,
|
|
76
|
+
"warn_pct": th.plan_usage_warn_pct,
|
|
77
|
+
"crit_pct": th.plan_usage_crit_pct,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return CheckResult(
|
|
82
|
+
check_id="plan_usage",
|
|
83
|
+
scope="machine",
|
|
84
|
+
severity=severity,
|
|
85
|
+
headroom=headroom,
|
|
86
|
+
threshold=f"crit at {th.plan_usage_crit_pct:.0f}%",
|
|
87
|
+
values=values,
|
|
88
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Per-checkout git hygiene: parked branch, uncommitted changes (#1628).
|
|
2
|
+
|
|
3
|
+
Both of these are the #561/#601 failure mode. A Build, a ``coord test``, or
|
|
4
|
+
an interactive agent inspecting a branch does ``git checkout`` in the *live*
|
|
5
|
+
base checkout and doesn't restore it. From then on the machine builds,
|
|
6
|
+
tests, and (if it's the editable checkout) *runs the coordinator itself*
|
|
7
|
+
from that branch's code, silently — #561 disabled guards that way, #601 ran
|
|
8
|
+
old code against a retired local DB. ``coord/cli.py`` already shouts about
|
|
9
|
+
this for the editable checkout on every command; this generalises it to
|
|
10
|
+
every checkout in ``coordinator.yml`` and puts it in the same report as the
|
|
11
|
+
rest of the fleet's headroom.
|
|
12
|
+
|
|
13
|
+
Uncommitted changes in a base checkout are the sibling signal: the merge
|
|
14
|
+
agent rebases and force-pushes in there, and a dirty tree makes that fail in
|
|
15
|
+
ways that read as a coordinator bug. Both are WARN, never CRIT — they are
|
|
16
|
+
recoverable in one command and a human is often *deliberately* mid-edit.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
25
|
+
from coord.health.registry import check
|
|
26
|
+
|
|
27
|
+
_GIT_TIMEOUT = 5.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _git(repo: Path, *args: str) -> tuple[int, str]:
|
|
31
|
+
"""``(returncode, stdout)`` — never raises for the ordinary failures.
|
|
32
|
+
|
|
33
|
+
stdout is returned **unstripped**: ``git status --porcelain`` encodes the
|
|
34
|
+
index/worktree state in the first two columns, so ``" M path"`` and
|
|
35
|
+
``"M path"`` mean different things and a ``.strip()`` here silently ate
|
|
36
|
+
the leading space (and with it one character of every path).
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
["git", "-C", str(repo), *args],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=_GIT_TIMEOUT,
|
|
44
|
+
)
|
|
45
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
46
|
+
return 1, f"{type(exc).__name__}: {exc}"
|
|
47
|
+
return result.returncode, result.stdout
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@check(
|
|
51
|
+
id="repo_branch",
|
|
52
|
+
scope="checkout",
|
|
53
|
+
title="branch",
|
|
54
|
+
order=60,
|
|
55
|
+
description="Base checkouts are on their default (or configured develop) branch.",
|
|
56
|
+
)
|
|
57
|
+
def probe_repo_branch(ctx: HealthContext) -> list[CheckResult]:
|
|
58
|
+
results: list[CheckResult] = []
|
|
59
|
+
for checkout in ctx.checkouts:
|
|
60
|
+
code, raw = _git(checkout.path, "rev-parse", "--abbrev-ref", "HEAD")
|
|
61
|
+
branch = raw.strip()
|
|
62
|
+
if code != 0:
|
|
63
|
+
results.append(
|
|
64
|
+
CheckResult(
|
|
65
|
+
check_id="repo_branch",
|
|
66
|
+
scope="checkout",
|
|
67
|
+
subject=checkout.name,
|
|
68
|
+
severity=Severity.UNKNOWN,
|
|
69
|
+
headroom="could not read HEAD",
|
|
70
|
+
error=branch,
|
|
71
|
+
values={"path": str(checkout.path)},
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
home = checkout.home_branches
|
|
77
|
+
detached = branch == "HEAD"
|
|
78
|
+
parked = detached or branch not in home
|
|
79
|
+
results.append(
|
|
80
|
+
CheckResult(
|
|
81
|
+
check_id="repo_branch",
|
|
82
|
+
scope="checkout",
|
|
83
|
+
subject=checkout.name,
|
|
84
|
+
severity=Severity.WARN if parked else Severity.OK,
|
|
85
|
+
headroom=("detached HEAD" if detached else branch)
|
|
86
|
+
+ (f" (expected {home[0]})" if parked else ""),
|
|
87
|
+
threshold="warn when off the default branch",
|
|
88
|
+
detail=(
|
|
89
|
+
f"restore with: git -C {checkout.path} checkout {home[0]}"
|
|
90
|
+
if parked
|
|
91
|
+
else ""
|
|
92
|
+
),
|
|
93
|
+
values={
|
|
94
|
+
"path": str(checkout.path),
|
|
95
|
+
"branch": branch,
|
|
96
|
+
"expected": list(home),
|
|
97
|
+
"detached": detached,
|
|
98
|
+
"parked": parked,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@check(
|
|
106
|
+
id="repo_dirty",
|
|
107
|
+
scope="checkout",
|
|
108
|
+
title="worktree clean",
|
|
109
|
+
order=61,
|
|
110
|
+
description="Base checkouts have no uncommitted changes.",
|
|
111
|
+
)
|
|
112
|
+
def probe_repo_dirty(ctx: HealthContext) -> list[CheckResult]:
|
|
113
|
+
results: list[CheckResult] = []
|
|
114
|
+
for checkout in ctx.checkouts:
|
|
115
|
+
# --porcelain is the stable machine format; --untracked-files=normal
|
|
116
|
+
# (the default) counts a stray build artifact as dirty, which is the
|
|
117
|
+
# behaviour we want — the merge agent's force-push trips on those too.
|
|
118
|
+
code, out = _git(checkout.path, "status", "--porcelain")
|
|
119
|
+
if code != 0:
|
|
120
|
+
results.append(
|
|
121
|
+
CheckResult(
|
|
122
|
+
check_id="repo_dirty",
|
|
123
|
+
scope="checkout",
|
|
124
|
+
subject=checkout.name,
|
|
125
|
+
severity=Severity.UNKNOWN,
|
|
126
|
+
headroom="could not read git status",
|
|
127
|
+
error=out,
|
|
128
|
+
values={"path": str(checkout.path)},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
lines = [line for line in out.splitlines() if line.strip()]
|
|
134
|
+
count = len(lines)
|
|
135
|
+
if count:
|
|
136
|
+
plural = "s" if count != 1 else ""
|
|
137
|
+
# Porcelain v1: two status columns, a space, then the path.
|
|
138
|
+
sample = ", ".join(line[3:].strip() for line in lines[:3])
|
|
139
|
+
if count > 3:
|
|
140
|
+
sample += ", …"
|
|
141
|
+
headroom = f"{count} uncommitted change{plural}"
|
|
142
|
+
if sample:
|
|
143
|
+
headroom = f"{headroom} ({sample})"
|
|
144
|
+
else:
|
|
145
|
+
headroom = "clean"
|
|
146
|
+
results.append(
|
|
147
|
+
CheckResult(
|
|
148
|
+
check_id="repo_dirty",
|
|
149
|
+
scope="checkout",
|
|
150
|
+
subject=checkout.name,
|
|
151
|
+
severity=Severity.WARN if count else Severity.OK,
|
|
152
|
+
headroom=headroom,
|
|
153
|
+
threshold="warn when dirty",
|
|
154
|
+
values={
|
|
155
|
+
"path": str(checkout.path),
|
|
156
|
+
"dirty_count": count,
|
|
157
|
+
"entries": lines[:20],
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return results
|