code-coordinator 0.5.46__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
@@ -0,0 +1,648 @@
1
+ """Systemd unit-file drift against `deploy/` (#1831).
2
+
3
+ `deploy/*.service`/`*.timer` is version-controlled, reviewed, and merged
4
+ like code. **Nothing ever installs it.** The release path is bump -> PR ->
5
+ merge -> tag push -> `publish.yml` -> PyPI, then `coord agent update` for the
6
+ venvs — no step copies `deploy/*.service` into `~/.config/systemd/user/`.
7
+ Unit files are hand-installed once at machine setup and drift forever after.
8
+
9
+ The 2026-08-04 incident this closes: dellserver's `coord-serve.service` was
10
+ three weeks stale, its `Environment=PATH=` still starting with an **editable**
11
+ checkout of this repo (`~/src/claude-coordinator/.venv/bin`). `coord_argv()`
12
+ (`coord/drive.py`) resolves subprocesses via `shutil.which("coord")` — i.e.
13
+ from that PATH — so the daemon itself ran the pinned release while
14
+ everything it spawned ran whatever stale branch that checkout happened to be
15
+ on. Two failure modes, one probe:
16
+
17
+ `unit_drift`
18
+ Per deploy-lane unit: does the installed copy under
19
+ `~/.config/systemd/user/` match `deploy/<name>`? Absence is the common
20
+ case (most machines don't run every lane) and is reported OK, not a
21
+ fault — same convention as `cli_venv`/`tui_binary`
22
+ (:mod:`coord.health.checks.deploy_lane_facts`).
23
+
24
+ `_path_shadow_risk`
25
+ Independent of content drift: does the installed unit's
26
+ `Environment=PATH=` put an editable checkout's `.venv/bin` ahead of the
27
+ release entry points (`~/.local/bin`, `~/.coord-venv/bin`)? This is what
28
+ made the drift above *harmful* rather than merely untidy, and it can
29
+ exist even on a unit whose content otherwise matches `deploy/` bit for
30
+ bit if `deploy/` itself regresses — which is exactly what happened to
31
+ `coord-serve.service`'s v0.4.105 cut (it dropped the #1117 PATH entry
32
+ that had also fixed a real bug). CRIT regardless of the content-diff
33
+ verdict — a shadowed release is the split-brain, not a cosmetic
34
+ difference.
35
+
36
+ #1927 — where the reference comes from
37
+ --------------------------------------
38
+ The original cut of this check diffed the installed unit against
39
+ ``<checkout>/deploy/<name>``: a file in the host's own git working copy that
40
+ nothing verifies is at the released tag, or current at all. Installed units
41
+ and checkouts go stale for the *same* reason (nobody pulled), so they go
42
+ stale *together* — and when they do the comparison reports clean. The check
43
+ was least reliable in exactly the case it exists to catch, and the remedy it
44
+ printed (``cp <checkout>/deploy/... ~/.config/systemd/user/...``) sourced
45
+ from the same unverified working copy, cementing the stale unit.
46
+
47
+ So the reference is now the *packaged* unit set — ``coord/deploy/`` inside
48
+ the installed distribution (see :func:`packaged_unit_dir`). That is the
49
+ released artifact for the version this process is running, and it cannot
50
+ drift with the host. When the reference is NOT a released artifact (an
51
+ editable/source checkout, a configured directory, or an old wheel that ships
52
+ no units) the verdict is annotated and a *match* grades UNKNOWN rather than
53
+ OK: an un-annotated green from an unverified reference is worse than no
54
+ check at all.
55
+
56
+ #1928 — ``coord-agent.service`` is a template, not a plain file
57
+ -----------------------------------------------------------------
58
+ Unlike its siblings (byte-for-byte ``cp``-installed from ``deploy/``),
59
+ ``deploy/coord-agent.service`` carries ``<MACHINE_NAME>``/``<PORT>``
60
+ placeholders that every real install fills in — via the documented manual
61
+ ``sed`` (leaving systemd's ``%h`` specifier alone) or via
62
+ ``install-agent.sh``'s inline heredoc (which expands ``%h`` to a literal
63
+ ``$HOME`` and drops the ~76-line doc-comment header entirely). A byte-diff
64
+ against the raw template can never match either of those, so it warned on
65
+ every host, permanently — and printed a bare ``cp`` remedy that, followed
66
+ verbatim, installs the placeholders as literal text and takes the unit
67
+ down.
68
+
69
+ ``_content_matches`` fixes this without weakening the check: it normalizes
70
+ away the two kinds of noise that are never a real difference — comment/blank
71
+ lines, and the ``%h`` vs. literal-``$HOME`` spelling — and then, only if the
72
+ (normalized) reference still contains placeholder tokens, accepts an
73
+ installed copy that fills them in *consistently* (the same placeholder
74
+ resolves to the same value everywhere it appears). A unit that is missing a
75
+ real property — e.g. elitebook's installed copy drops ``--machine``/
76
+ ``--port`` from ``ExecStart`` entirely rather than filling them in — still
77
+ fails the match and still warns; that is a real defect, not template noise.
78
+ The remedy for a still-drifting *templated* unit is never the bare ``cp``:
79
+ see :func:`_templated_remedy`.
80
+ """
81
+
82
+ from __future__ import annotations
83
+
84
+ import difflib
85
+ import re
86
+ from dataclasses import dataclass
87
+ from pathlib import Path
88
+
89
+ from coord.health.models import CheckResult, HealthContext, Severity
90
+ from coord.health.registry import check
91
+ from coord.health.units import expand, human_hours
92
+
93
+ _UNIT_GLOBS = ("*.service", "*.timer")
94
+ _SYSTEMD_USER_DIR = "~/.config/systemd/user"
95
+
96
+ # Entry points that resolve to the pinned release. A `.venv/bin` entry ahead
97
+ # of ALL of these on a unit's PATH can shadow it (#1831's dellserver case).
98
+ _RELEASE_MARKERS = ("/.local/bin", "/.coord-venv/bin")
99
+
100
+ _PATH_LINE_RE = re.compile(r"^Environment\s*=\s*PATH=(.*)$", re.MULTILINE)
101
+
102
+ # #1928: a template placeholder, e.g. `<MACHINE_NAME>` or `<PORT>` — see
103
+ # `deploy/coord-agent.service`. Uppercase-with-underscores by convention so
104
+ # it can never collide with a real systemd directive or value.
105
+ _PLACEHOLDER_RE = re.compile(r"<([A-Z][A-Z0-9_]*)>")
106
+
107
+ # Placeholders this module knows a real, safe substitution for — used to
108
+ # build a runnable remedy in `_templated_remedy` instead of a bare `cp`
109
+ # (#1928). Anything not listed here still gets a safe remedy, just not one
110
+ # that reaches for a value this module has no business guessing.
111
+ _KNOWN_PLACEHOLDER_VALUES = {
112
+ "MACHINE_NAME": "$(hostname -s)",
113
+ "PORT": "7433",
114
+ }
115
+
116
+ # Sentinel used to fold systemd's `%h` specifier and this host's literal
117
+ # $HOME into one token before comparing unit text (#1928) — install-agent.sh
118
+ # expands $HOME to a literal path in the unit it writes; deploy/*.service
119
+ # and a manual sed-install both keep `%h` literal. Both are correct, so
120
+ # neither spelling may count as drift. Chosen to be inert under re.escape()
121
+ # (see `_placeholder_pattern`) and never appear in a real unit file.
122
+ _HOME_TOKEN = "\x00HOME\x00"
123
+
124
+
125
+ def packaged_unit_dir():
126
+ """`coord/deploy/` inside *this* installed distribution, or None.
127
+
128
+ Shipped as package data (see `pyproject.toml`), so on a pip-installed
129
+ host it is the unit set as of the installed version — the released
130
+ artifact, which cannot drift with the host's git checkout (#1927).
131
+ Returns None on a wheel old enough to predate #1927, which is why the
132
+ working-copy fallbacks below still exist.
133
+ """
134
+ candidate = Path(__file__).resolve().parent.parent.parent / "deploy"
135
+ if candidate.is_dir() and _unit_files(candidate):
136
+ return candidate
137
+ return None
138
+
139
+
140
+ def in_git_worktree(path: Path) -> bool:
141
+ """Is `path` inside a git working copy?
142
+
143
+ The discriminator between "released artifact" and "working copy" for
144
+ :func:`packaged_unit_dir`: an editable/source install puts the package
145
+ under a checkout, where `coord/deploy/` is as unverified as any other
146
+ tracked file. A pip-installed wheel lands in `site-packages`, which has
147
+ no `.git` above it.
148
+ """
149
+ try:
150
+ resolved = path.resolve()
151
+ except OSError: # pragma: no cover - resolve() is effectively total here
152
+ return False
153
+ for parent in (resolved, *resolved.parents):
154
+ try:
155
+ if (parent / ".git").exists():
156
+ return True
157
+ except OSError: # pragma: no cover - unreadable ancestor
158
+ continue
159
+ return False
160
+
161
+
162
+ def installed_version() -> str | None:
163
+ """The version of the installed coordinator distribution, or None.
164
+
165
+ #2103: resolves via :func:`coord.dist_name.resolve_installed` — tries
166
+ `code-coordinator` then falls back to `claude-coordinator` — instead of
167
+ a single hardcoded name, so this (feeding the unit-drift health check)
168
+ doesn't start reporting a false "not installed" the moment a machine
169
+ lands on the new name.
170
+ """
171
+ try:
172
+ from coord.dist_name import resolve_installed
173
+
174
+ return resolve_installed().version
175
+ except Exception: # pragma: no cover - metadata missing in odd installs
176
+ return None
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class UnitReference:
181
+ """The thing installed units are diffed against, and how much it's worth.
182
+
183
+ `verified` is the whole point of #1927: only a reference that is the
184
+ released artifact for the installed version can turn a match into a
185
+ green. Everything else is a working copy whose own currency is unknown,
186
+ so its match is reported as UNKNOWN with `label` naming what was
187
+ actually compared.
188
+ """
189
+
190
+ path: Path
191
+ source: str # "package" | "configured" | "checkout"
192
+ verified: bool
193
+ version: str | None = None
194
+
195
+ @property
196
+ def label(self) -> str:
197
+ if self.source == "package":
198
+ ver = f" {self.version}" if self.version else ""
199
+ if self.verified:
200
+ return f"the packaged units of installed coord{ver}"
201
+ return f"coord{ver}'s packaged units (SOURCE CHECKOUT, unverified)"
202
+ if self.source == "configured":
203
+ return f"configured reference {self.path} (unverified working copy)"
204
+ return f"{self.path} (unverified working copy)"
205
+
206
+ @property
207
+ def short_label(self) -> str:
208
+ """The `headroom` half of :attr:`label` — one line, no path."""
209
+ if self.source == "package" and self.verified:
210
+ return f"packaged coord{' ' + self.version if self.version else ''}"
211
+ return f"{self.path}"
212
+
213
+
214
+ def resolve_reference(ctx: HealthContext) -> UnitReference | None:
215
+ """Where to read the reference units from, and whether it's trustworthy.
216
+
217
+ Order (#1927): the packaged units of the running distribution first —
218
+ they are the released artifact and are the only reference that cannot go
219
+ stale with the host. `health.deploy_dir` and then the first local
220
+ checkout's `deploy/` remain as fallbacks for wheels that predate #1927
221
+ (and for operators who deliberately point the check elsewhere), but both
222
+ are working copies and are flagged as such.
223
+ """
224
+ packaged = packaged_unit_dir()
225
+ if packaged is not None:
226
+ return UnitReference(
227
+ path=packaged,
228
+ source="package",
229
+ verified=not in_git_worktree(packaged),
230
+ version=installed_version(),
231
+ )
232
+ fallback = resolve_deploy_dir(ctx)
233
+ if fallback is None:
234
+ return None
235
+ configured = getattr(ctx.thresholds, "deploy_dir", None)
236
+ return UnitReference(
237
+ path=fallback,
238
+ source="configured" if configured else "checkout",
239
+ verified=False,
240
+ )
241
+
242
+
243
+ def resolve_deploy_dir(ctx: HealthContext):
244
+ """The checked-in `deploy/` this machine can diff installed units against.
245
+
246
+ The #1927 *fallback* reference, used only when the installed
247
+ distribution ships no `coord/deploy/` of its own — see
248
+ :func:`resolve_reference`, which is what the probe calls.
249
+
250
+ Configured `health.deploy_dir` wins outright; otherwise the first local
251
+ checkout (see `coord.health.context.local_checkouts`) that has one —
252
+ normally the `claude-coordinator` entry in `repo_paths`.
253
+ """
254
+ configured = getattr(ctx.thresholds, "deploy_dir", None)
255
+ if configured:
256
+ return expand(configured, ctx.home)
257
+ for checkout in ctx.checkouts:
258
+ candidate = checkout.path / "deploy"
259
+ if candidate.is_dir():
260
+ return candidate
261
+ return None
262
+
263
+
264
+ def resolve_systemd_user_dir(ctx: HealthContext):
265
+ """Where installed systemd *user* units actually live on this machine."""
266
+ configured = getattr(ctx.thresholds, "systemd_user_dir", None)
267
+ if configured:
268
+ return expand(configured, ctx.home)
269
+ return expand(_SYSTEMD_USER_DIR, ctx.home)
270
+
271
+
272
+ def _unit_files(deploy_dir):
273
+ seen = set()
274
+ out = []
275
+ for pattern in _UNIT_GLOBS:
276
+ for path in sorted(deploy_dir.glob(pattern)):
277
+ if path.name in seen:
278
+ continue
279
+ seen.add(path.name)
280
+ out.append(path)
281
+ return sorted(out, key=lambda p: p.name)
282
+
283
+
284
+ def _strip_noise_lines(text: str) -> str:
285
+ """Drop comment and blank lines.
286
+
287
+ Doc prose that never affects what systemd actually runs — and the
288
+ biggest source of #1928's false drift: `deploy/coord-agent.service`
289
+ carries ~76 lines of install documentation that `install-agent.sh`'s
290
+ generated unit never includes, so a raw byte-diff always "differed" on
291
+ that alone, template placeholders aside.
292
+ """
293
+ return "\n".join(
294
+ line for line in text.splitlines() if line.strip() and not line.strip().startswith("#")
295
+ )
296
+
297
+
298
+ def _normalize_home(text: str, home: Path) -> str:
299
+ """Fold systemd's `%h` specifier and this host's literal $HOME into one
300
+ token (#1928), so a unit that spells its home directory either way
301
+ compares equal. `install-agent.sh`'s heredoc expands `$HOME` to a
302
+ literal path when it writes the unit; `deploy/coord-agent.service` and a
303
+ manual sed-install both leave `%h` for systemd to resolve at run time.
304
+ Both are correct — neither spelling is drift.
305
+ """
306
+ text = text.replace("%h", _HOME_TOKEN)
307
+ home_str = str(home)
308
+ if home_str and home_str != "/":
309
+ text = text.replace(home_str, _HOME_TOKEN)
310
+ return text
311
+
312
+
313
+ def _normalize_unit_text(text: str, home: Path) -> str:
314
+ """Strip the noise `_content_matches` never treats as drift (#1928)."""
315
+ return _strip_noise_lines(_normalize_home(text, home))
316
+
317
+
318
+ def _placeholder_pattern(normalized_deploy_text: str) -> re.Pattern[str]:
319
+ """Compile *normalized_deploy_text* into a regex matching any rendering
320
+ of its `<PLACEHOLDER>` tokens — each becomes a capture group on first
321
+ use and a backreference on repeat, so the same placeholder must resolve
322
+ to the same value everywhere it appears (#1928: `<PORT>` shows up in
323
+ both `Description=` and `ExecStart=` in `deploy/coord-agent.service`).
324
+
325
+ `re.escape()` leaves `<`, `>`, letters, digits and `_` untouched (see
326
+ module-level note on `_HOME_TOKEN`), so the placeholder tokens and the
327
+ home sentinel both survive escaping intact and are still findable.
328
+ """
329
+ seen: set[str] = set()
330
+
331
+ def repl(m: re.Match[str]) -> str:
332
+ name = m.group(1)
333
+ if name in seen:
334
+ return f"(?P={name})"
335
+ seen.add(name)
336
+ return f"(?P<{name}>.+?)"
337
+
338
+ escaped = re.escape(normalized_deploy_text)
339
+ return re.compile(_PLACEHOLDER_RE.sub(repl, escaped), re.DOTALL)
340
+
341
+
342
+ def _content_matches(deploy_text: str, installed_text: str, home: Path) -> bool:
343
+ """Does *installed_text* satisfy *deploy_text* (#1928)?
344
+
345
+ Exact, modulo the noise `_normalize_unit_text` strips, when
346
+ *deploy_text* is a plain file (every lane but `coord-agent.service`
347
+ today). When *deploy_text* is a template — it still contains
348
+ `<PLACEHOLDER>` tokens after normalizing — an installed copy that fills
349
+ them in consistently also counts as a match: the placeholder is the
350
+ only thing allowed to differ, so a unit that's missing a real property
351
+ (e.g. `ExecStart` dropping `--machine`/`--port` entirely rather than
352
+ filling them in) still fails to match and still reports drift.
353
+ """
354
+ deploy_norm = _normalize_unit_text(deploy_text, home)
355
+ installed_norm = _normalize_unit_text(installed_text, home)
356
+ if deploy_norm == installed_norm:
357
+ return True
358
+ if not _PLACEHOLDER_RE.search(deploy_norm):
359
+ return False
360
+ return _placeholder_pattern(deploy_norm).fullmatch(installed_norm) is not None
361
+
362
+
363
+ def _is_templated(deploy_text: str) -> bool:
364
+ """Does the *raw* reference text contain unfilled placeholders (#1928)?
365
+
366
+ Checked against the raw text, not the normalized one, purely so this
367
+ reads naturally at call sites that haven't normalized anything yet —
368
+ normalization never introduces or removes a `<PLACEHOLDER>` token.
369
+ """
370
+ return _PLACEHOLDER_RE.search(deploy_text) is not None
371
+
372
+
373
+ def _templated_remedy(deploy_text: str, deploy_path: Path, installed_path: Path, service: str) -> str:
374
+ """The remedy for a still-drifting *templated* unit (#1928).
375
+
376
+ Never the bare `cp` the non-template branch prints: run verbatim, that
377
+ installs `<MACHINE_NAME>`/`<PORT>` as literal text and the unit refuses
378
+ to start — which is this issue's entire complaint. When every
379
+ placeholder in the template has a known, safe substitution (today:
380
+ `MACHINE_NAME`, `PORT`) the remedy is a real, runnable `sed` — copy-paste
381
+ safe, per #1928's acceptance criteria. If some future placeholder has no
382
+ known substitution, fall back to a pointer at the template's own
383
+ documented install procedure rather than guess a value.
384
+ """
385
+ names = sorted(set(_PLACEHOLDER_RE.findall(deploy_text)))
386
+ if names and all(n in _KNOWN_PLACEHOLDER_VALUES for n in names):
387
+ sed_args = " ".join(f'-e "s/<{n}>/{_KNOWN_PLACEHOLDER_VALUES[n]}/"' for n in names)
388
+ return (
389
+ f"{deploy_path} is a TEMPLATE — do not cp it verbatim (#1928). Render "
390
+ f"it for this host first: sed {sed_args} {deploy_path} > {installed_path} "
391
+ f"&& systemctl --user daemon-reload && systemctl --user restart {service}"
392
+ )
393
+ return (
394
+ f"{deploy_path} is a TEMPLATE ({', '.join(names)} placeholder(s)) — copying "
395
+ "it verbatim installs those as literal text and the unit will not start. "
396
+ f"See the install instructions at the top of {deploy_path} (sed substitution "
397
+ "or install-agent.sh) to render it for this host before installing."
398
+ )
399
+
400
+
401
+ def _diff_summary(installed_text: str, deploy_text: str) -> tuple[int, int | None]:
402
+ """(changed line count, first differing line number in the installed
403
+ file) between two unit files — a cheap stand-in for a full diff in a
404
+ one-line `headroom` string."""
405
+ diff = list(
406
+ difflib.unified_diff(
407
+ installed_text.splitlines(), deploy_text.splitlines(), lineterm=""
408
+ )
409
+ )
410
+ changed = sum(
411
+ 1 for line in diff if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))
412
+ )
413
+ first_line = None
414
+ for line in diff:
415
+ m = re.match(r"@@ -(\d+)", line)
416
+ if m:
417
+ first_line = int(m.group(1))
418
+ break
419
+ return changed, first_line
420
+
421
+
422
+ def find_path_shadow(installed_text: str) -> str | None:
423
+ """The PATH entry that shadows the release, or None if the installed
424
+ unit's PATH is safe (no editable checkout ahead of a release marker).
425
+
426
+ A "release marker" is `~/.local/bin` or `~/.coord-venv/bin` — either
427
+ resolves `coord` to the pinned install (`~/.local/bin/coord` is a
428
+ symlink onto `~/.coord-venv/bin/coord`). An entry whose LAST path
429
+ component is `.venv/bin` (a project-local dev venv, as opposed to the
430
+ dot-prefixed-but-distinct `.coord-venv`/`.coord-cli-venv`) ahead of that
431
+ marker is exactly the #1831 split-brain: `shutil.which("coord")`
432
+ (`coord_argv()`, `coord/drive.py`) resolves it first.
433
+
434
+ Only the LAST `Environment=PATH=` directive is read — systemd unit files
435
+ may repeat `Environment=`, and later directives for the same key are
436
+ what actually take effect.
437
+ """
438
+ matches = _PATH_LINE_RE.findall(installed_text)
439
+ if not matches:
440
+ return None
441
+ entries = [e for e in matches[-1].split(":") if e]
442
+
443
+ release_idx = None
444
+ for idx, entry in enumerate(entries):
445
+ stripped = entry.rstrip("/")
446
+ if any(stripped.endswith(marker) for marker in _RELEASE_MARKERS):
447
+ release_idx = idx
448
+ break
449
+
450
+ for idx, entry in enumerate(entries):
451
+ if release_idx is not None and idx >= release_idx:
452
+ break
453
+ if entry.rstrip("/").endswith("/.venv/bin"):
454
+ return entry
455
+ return None
456
+
457
+
458
+ @check(
459
+ id="unit_drift",
460
+ scope="machine",
461
+ title="unit drift",
462
+ order=44,
463
+ description=(
464
+ "Installed systemd user units (~/.config/systemd/user/) match the "
465
+ "units packaged with the installed release, and no unit's PATH lets "
466
+ "an editable checkout shadow that release (#1831, #1927)."
467
+ ),
468
+ )
469
+ def probe_unit_drift(ctx: HealthContext) -> list[CheckResult]:
470
+ reference = resolve_reference(ctx)
471
+ if reference is None:
472
+ return [
473
+ CheckResult(
474
+ check_id="unit_drift",
475
+ scope="machine",
476
+ severity=Severity.OK,
477
+ headroom="no deploy/ checkout found on this machine",
478
+ values={"deploy_dir": None, "reference_source": None},
479
+ )
480
+ ]
481
+
482
+ deploy_dir = reference.path
483
+ installed_dir = resolve_systemd_user_dir(ctx)
484
+ results: list[CheckResult] = []
485
+ for deploy_path in _unit_files(deploy_dir):
486
+ name = deploy_path.name
487
+ installed_path = installed_dir / name
488
+ values: dict = {
489
+ "deploy_path": str(deploy_path),
490
+ "installed_path": str(installed_path),
491
+ # #1927: a green is only interpretable alongside what produced
492
+ # it, so every result carries its reference — and whether that
493
+ # reference is the released artifact or a working copy.
494
+ "reference_dir": str(reference.path),
495
+ "reference_source": reference.source,
496
+ "reference_verified": reference.verified,
497
+ "reference_version": reference.version,
498
+ }
499
+
500
+ if not installed_path.exists():
501
+ results.append(
502
+ CheckResult(
503
+ check_id="unit_drift",
504
+ scope="machine",
505
+ subject=name,
506
+ severity=Severity.OK,
507
+ headroom="not installed on this machine",
508
+ values={**values, "installed": False},
509
+ )
510
+ )
511
+ continue
512
+
513
+ try:
514
+ deploy_text = deploy_path.read_text()
515
+ installed_text = installed_path.read_text()
516
+ installed_mtime = installed_path.stat().st_mtime
517
+ except OSError as exc:
518
+ results.append(
519
+ CheckResult(
520
+ check_id="unit_drift",
521
+ scope="machine",
522
+ subject=name,
523
+ severity=Severity.UNKNOWN,
524
+ headroom=f"could not read unit: {exc}",
525
+ error=str(exc),
526
+ values={**values, "installed": True},
527
+ )
528
+ )
529
+ continue
530
+
531
+ values["installed"] = True
532
+ values["installed_mtime"] = installed_mtime
533
+ # #1928: not a bare `==` — `deploy_text` may be a template (today,
534
+ # only coord-agent.service), and even for a plain file, comment/blank
535
+ # lines and %h-vs-literal-$HOME spelling are never real drift.
536
+ templated = _is_templated(deploy_text)
537
+ matches = _content_matches(deploy_text, installed_text, ctx.home)
538
+ values["matches"] = matches
539
+ values["templated"] = templated
540
+ shadow_entry = find_path_shadow(installed_text)
541
+ values["shadow_entry"] = shadow_entry
542
+
543
+ if shadow_entry:
544
+ age = ctx.now - installed_mtime
545
+ detail = (
546
+ f"editable checkout '{shadow_entry}' precedes the release "
547
+ "entry point on this unit's PATH — shutil.which(\"coord\") in "
548
+ "subprocesses this unit spawns resolves the checkout instead "
549
+ "of the pinned release (#1831). Reorder PATH= so ~/.local/bin "
550
+ "or ~/.coord-venv/bin comes first."
551
+ )
552
+ results.append(
553
+ CheckResult(
554
+ check_id="unit_drift",
555
+ scope="machine",
556
+ subject=name,
557
+ severity=Severity.CRIT,
558
+ headroom=f"PATH shadow risk ({human_hours(age)} since install)",
559
+ detail=detail,
560
+ threshold="crit when a .venv/bin entry precedes ~/.local/bin or ~/.coord-venv/bin",
561
+ values=values,
562
+ )
563
+ )
564
+ continue
565
+
566
+ if not matches:
567
+ changed, first_line = _diff_summary(installed_text, deploy_text)
568
+ age = ctx.now - installed_mtime
569
+ values["diff_lines"] = changed
570
+ values["first_diff_line"] = first_line
571
+ where = f", first differing at line {first_line}" if first_line else ""
572
+ # #1928: a templated reference (coord-agent.service) never gets
573
+ # the bare `cp` remedy below — followed verbatim it installs
574
+ # `<MACHINE_NAME>`/`<PORT>` as literal text and takes the unit
575
+ # down. See `_templated_remedy`.
576
+ if templated:
577
+ detail = (
578
+ f"{_templated_remedy(deploy_text, deploy_path, installed_path, name.rsplit('.', 1)[0])}"
579
+ f" # reference: {reference.label}"
580
+ )
581
+ else:
582
+ # The remedy sources from the SAME file the diff read
583
+ # (#1927) — a `cp` out of an unverified checkout is how a
584
+ # stale unit got cemented in the first place.
585
+ detail = (
586
+ f"cp {deploy_path} {installed_path} && systemctl --user "
587
+ f"daemon-reload && systemctl --user restart "
588
+ f"{name.rsplit('.', 1)[0]} # reference: {reference.label}"
589
+ )
590
+ results.append(
591
+ CheckResult(
592
+ check_id="unit_drift",
593
+ scope="machine",
594
+ subject=name,
595
+ severity=Severity.WARN,
596
+ headroom=(
597
+ f"stale — installed {human_hours(age)} ago, {changed} "
598
+ f"line(s) differ from {reference.short_label}"
599
+ f"{where}"
600
+ ),
601
+ detail=detail,
602
+ threshold=f"warn when installed content != {reference.short_label}",
603
+ values=values,
604
+ )
605
+ )
606
+ continue
607
+
608
+ if not reference.verified:
609
+ # Content matches, but the reference is a working copy nothing
610
+ # verified is current (#1927). Reporting OK here is the exact
611
+ # false green this check was rebuilt to stop emitting: a stale
612
+ # checkout and a stale installed unit agree with each other.
613
+ results.append(
614
+ CheckResult(
615
+ check_id="unit_drift",
616
+ scope="machine",
617
+ subject=name,
618
+ severity=Severity.UNKNOWN,
619
+ headroom=(
620
+ f"matches {reference.short_label}, but that reference "
621
+ "is an unverified working copy — cannot confirm this "
622
+ "is the released unit"
623
+ ),
624
+ detail=(
625
+ f"diffed against {reference.label}. Nothing checks "
626
+ "that copy is at the released tag, and an installed "
627
+ "unit drifts for the same reason a checkout does, so "
628
+ "the two go stale together and agree (#1927). Install "
629
+ "a release wheel on this host (it ships coord/deploy/) "
630
+ "to make this comparison meaningful."
631
+ ),
632
+ values=values,
633
+ )
634
+ )
635
+ continue
636
+
637
+ results.append(
638
+ CheckResult(
639
+ check_id="unit_drift",
640
+ scope="machine",
641
+ subject=name,
642
+ severity=Severity.OK,
643
+ headroom=f"matches {reference.short_label}",
644
+ values=values,
645
+ )
646
+ )
647
+
648
+ return results