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
coord/cli.py ADDED
@@ -0,0 +1,454 @@
1
+ """Click CLI entry point for the `coord` command.
2
+
3
+ #747: this module just builds the `main`/`agent`/`issue`/`context` groups and
4
+ registers the commands implemented in coord/commands/*.py — the actual
5
+ command bodies (~70 commands across ~12k lines pre-#747) now live in those
6
+ focused modules. Keep this file thin: new commands belong in
7
+ coord/commands/<area>.py, imported and attached here in one place.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # Compatibility shims, not used directly by this file: a number of existing
13
+ # tests patch e.g. "coord.cli.subprocess.run" / "coord.cli.socket.gethostname"
14
+ # / "coord.cli.httpx.get" / "coord.cli.Path.home". Those patches work by
15
+ # replacing an attribute on the *shared* stdlib/third-party module or class
16
+ # object (the same object every coord.commands.* module below imports), so
17
+ # keeping these imports here — even though cli.py itself no longer calls
18
+ # them — keeps those existing test-patch targets resolving. noqa: F401
19
+ import os # noqa: F401
20
+ import shutil # noqa: F401
21
+ import socket # noqa: F401
22
+ import subprocess # noqa: F401
23
+ import time # noqa: F401
24
+ from pathlib import Path # noqa: F401
25
+
26
+ import click
27
+ import httpx # noqa: F401
28
+
29
+ from coord import __version__
30
+ from coord.dist_name import pkg_spec as _dist_pkg_spec
31
+
32
+ from coord.commands.acceptance import acceptance_group
33
+ from coord.commands.audit import audit
34
+ from coord.commands.gate_a import gate_a
35
+ from coord.commands.report import report_group
36
+ from coord.commands.scorecard import scorecard
37
+
38
+ # Re-exported for back-compat: some tests do `from coord.cli import
39
+ # _save_config_snapshot` / `_load_config` / etc. directly.
40
+ from coord.commands._common import ( # noqa: F401
41
+ AGENT_PORT,
42
+ SERVE_PORT,
43
+ _apply_label_change,
44
+ _CONFIG_OPTION,
45
+ _load_config,
46
+ _not_implemented,
47
+ _save_config_snapshot,
48
+ )
49
+
50
+ from coord.commands.setup import (
51
+ _ensure_coord_permissions, # noqa: F401 — re-exported for tests
52
+ _parse_github_remote, # noqa: F401 — re-exported for tests
53
+ config_cmd,
54
+ init,
55
+ install_skills,
56
+ version,
57
+ )
58
+ from coord.commands.agent_ops import agent, pause, unpause
59
+ from coord.commands.gates import gates
60
+ from coord.commands.status import diagnose, doctor, show_plan, status, usage
61
+ from coord.commands.dispatch import (
62
+ approve,
63
+ assign,
64
+ chat_continue,
65
+ inject,
66
+ plan,
67
+ retry,
68
+ stop,
69
+ )
70
+ from coord.commands.sessions import (
71
+ _prune_dead_sessions, # noqa: F401 — re-exported for tests
72
+ log,
73
+ pull_artifact,
74
+ reattach,
75
+ session,
76
+ sessions_cmd,
77
+ wait,
78
+ watch,
79
+ )
80
+ from coord.commands.terminal import terminal_group
81
+ from coord.commands.tui import tui_group
82
+
83
+ # #1628: the health check engine ships its own CLI surface inside
84
+ # coord/health/ so the whole feature — registry, probes, renderer, command —
85
+ # stays in one package. Registered here like any other command.
86
+ from coord.health.cli import health
87
+ from coord.commands.merge import (
88
+ bounce,
89
+ merge,
90
+ post_pending_reviews,
91
+ reconcile_merges,
92
+ verify_merge,
93
+ )
94
+ from coord.commands.review import (
95
+ _prompt_and_relay_review_verdict, # noqa: F401 — re-exported for tests
96
+ _prompt_and_relay_test_verdict, # noqa: F401 — re-exported for tests
97
+ fix_briefing_cmd,
98
+ report_result,
99
+ review_reaffirm,
100
+ set_review_findings,
101
+ )
102
+ from coord.commands.test_gate import (
103
+ _get_assignment_branch_head, # noqa: F401 — re-exported for tests
104
+ set_test_mode,
105
+ test,
106
+ test_plan_cmd,
107
+ )
108
+ from coord.commands.chat import (
109
+ new_issue_chat,
110
+ ready,
111
+ refine,
112
+ refine_board,
113
+ refine_chat,
114
+ test_chat,
115
+ )
116
+ from coord.commands.issues import (
117
+ backlog,
118
+ context_group,
119
+ issue_group,
120
+ queue,
121
+ sync,
122
+ track,
123
+ unqueue,
124
+ untrack,
125
+ )
126
+ from coord.commands.drive import (
127
+ drive,
128
+ drive_attach,
129
+ drive_sessions,
130
+ drive_stop,
131
+ escalate_group,
132
+ )
133
+ from coord.commands.drive_queue import drive_queue_group
134
+ from coord.commands.lifecycle import done, housekeeping, notify, resume, serve, web
135
+ from coord.commands.milestone import milestone_group
136
+ from coord.commands.plans import plans_cmd
137
+ from coord.commands.release import release_group, release_preflight
138
+ from coord.commands.plan_followup import (
139
+ _dispatch_followup, # noqa: F401 — re-exported for tests
140
+ approve_plan,
141
+ fix,
142
+ pr,
143
+ reject_plan,
144
+ resume_stuck,
145
+ review,
146
+ split,
147
+ )
148
+
149
+
150
+ # #1182: thresholds past which a stale non-editable install escalates from the
151
+ # mild "edits won't reach the CLI" note to a loud STALE INSTALL banner.
152
+ _STALE_COMMITS_THRESHOLD = 3
153
+ _STALE_DAYS_THRESHOLD = 2.0
154
+
155
+
156
+ def _compute_install_staleness(repo_root: Path, installed_version: str) -> dict | None:
157
+ """Best-effort: how far *installed_version* trails ``repo_root``'s HEAD.
158
+
159
+ Returns ``{"commits_behind": int, "days_behind": float | None, "tag": str}``
160
+ when a ``v{installed_version}`` tag exists in the checkout and HEAD has
161
+ moved past it, or ``None`` when there's no matching tag, the checkout
162
+ isn't a git repo, HEAD hasn't moved, or anything else goes wrong (never
163
+ raises — this is a nice-to-have signal, not something that should ever
164
+ break the CLI, per #1182).
165
+ """
166
+ import subprocess # noqa: PLC0415
167
+ import time # noqa: PLC0415
168
+
169
+ if not installed_version:
170
+ return None
171
+ try:
172
+ if not (repo_root / ".git").exists():
173
+ return None
174
+ tag = f"v{installed_version}"
175
+
176
+ def _git(*args: str) -> str | None:
177
+ result = subprocess.run(
178
+ ["git", "-C", str(repo_root), *args],
179
+ capture_output=True, text=True, timeout=5,
180
+ )
181
+ if result.returncode != 0:
182
+ return None
183
+ return result.stdout.strip()
184
+
185
+ if _git("rev-parse", "-q", "--verify", f"refs/tags/{tag}") is None:
186
+ return None # no matching tag — can't quantify drift
187
+ count_raw = _git("rev-list", "--count", f"{tag}..HEAD")
188
+ if count_raw is None or not count_raw.isdigit():
189
+ return None
190
+ commits_behind = int(count_raw)
191
+ if commits_behind <= 0:
192
+ return None # installed version is at least as new as HEAD
193
+
194
+ days_behind = None
195
+ ts_raw = _git("log", "-1", "--format=%ct", tag)
196
+ if ts_raw and ts_raw.isdigit():
197
+ days_behind = (time.time() - int(ts_raw)) / 86400.0
198
+ return {"commits_behind": commits_behind, "days_behind": days_behind, "tag": tag}
199
+ except Exception: # noqa: BLE001 — best-effort, never break the CLI
200
+ return None
201
+
202
+
203
+ def _warn_if_source_install_drift() -> None:
204
+ """Warn when the CLI is running from a non-editable install of a package
205
+ whose source checkout is the current working directory.
206
+
207
+ Root cause of #222: ``pip install .`` (without ``-e``) copies a snapshot
208
+ into site-packages. Subsequent edits in the source tree don't reach the
209
+ CLI, while ``python -c "from coord.... import ..."`` from the source dir
210
+ DOES pick them up (cwd shadows site-packages on import). Result: the same
211
+ workflow gives different answers depending on entry path.
212
+
213
+ Heuristic: ``coord.__file__`` lives in ``site-packages`` AND the cwd has
214
+ a sibling ``coord/`` package — that's exactly the drift case.
215
+
216
+ #1182: that plain warning shipped in #222 turned out to read as
217
+ boilerplate noise — it fired identically whether the install was a day
218
+ stale or, as happened on elitebook, many releases behind `main` (silently
219
+ evaluating retired logic and causing a false merge-gate block). When the
220
+ checkout has a tag matching the installed version, we now also quantify
221
+ how many commits/days HEAD has moved past it and escalate to a much
222
+ louder banner past ``_STALE_COMMITS_THRESHOLD`` / ``_STALE_DAYS_THRESHOLD``.
223
+ """
224
+ import os # noqa: PLC0415
225
+
226
+ try:
227
+ import coord as _coord # noqa: PLC0415
228
+
229
+ coord_file = _coord.__file__ or ""
230
+ if "site-packages" not in coord_file:
231
+ return # Editable install — source IS the import path, no drift.
232
+ local_init = Path(os.getcwd()) / "coord" / "__init__.py"
233
+ if not local_init.exists():
234
+ return # Not running from a source checkout.
235
+
236
+ staleness = _compute_install_staleness(Path(os.getcwd()), _coord.__version__ or "")
237
+ if staleness is not None and (
238
+ staleness["commits_behind"] >= _STALE_COMMITS_THRESHOLD
239
+ or (
240
+ staleness["days_behind"] is not None
241
+ and staleness["days_behind"] >= _STALE_DAYS_THRESHOLD
242
+ )
243
+ ):
244
+ days_note = (
245
+ f", ~{staleness['days_behind']:.1f} day(s)"
246
+ if staleness["days_behind"] is not None
247
+ else ""
248
+ )
249
+ # #2103: suggest an upgrade of whichever distribution name is
250
+ # actually installed — `code-coordinator` since the #2104 rename,
251
+ # `claude-coordinator` on an agent that has not been updated past
252
+ # it yet. `_coord.__file__` is already confirmed above to be a
253
+ # real site-packages install, so one of the two names is
254
+ # guaranteed to resolve here; the literal fallback only guards a
255
+ # resolution race between that check and this one. It names the
256
+ # NEW distribution (#2104): the only way to reach it is "neither
257
+ # name resolved", and the one name guaranteed to still be
258
+ # publishable is the one every future release ships under —
259
+ # `claude-coordinator` is a tombstone that will never gain
260
+ # another version. `_dist_pkg_spec` is the module-level
261
+ # `coord.dist_name` import above (real module, independent of
262
+ # `_coord` here potentially being a test stand-in), so this
263
+ # doesn't depend on `coord.dist_name` already being cached.
264
+ try:
265
+ install_target = _dist_pkg_spec(extra="server")
266
+ except Exception: # noqa: BLE001 — best-effort, never break the CLI
267
+ install_target = "code-coordinator[server]"
268
+ banner = "⚠" * 24
269
+ click.echo(
270
+ f"{banner}\n"
271
+ f"⚠⚠⚠ STALE INSTALL: coord CLI is {staleness['commits_behind']} "
272
+ f"commit(s){days_note} behind the checkout's HEAD ⚠⚠⚠\n"
273
+ f"Installed: {_coord.__version__} (tag {staleness['tag']}) — the source "
274
+ f"checkout at {local_init.parent} has moved past that release.\n"
275
+ "This install may be silently evaluating RETIRED logic (#1182 — this is "
276
+ "how a false merge-gate block slipped through). Fix:\n"
277
+ f" pip install --upgrade '{install_target}' "
278
+ "(if a release covers those commits)\n"
279
+ " pip install -e . (to run live source instead)\n"
280
+ f"{banner}",
281
+ err=True,
282
+ )
283
+ return
284
+
285
+ # Inside a source checkout but CLI uses snapshot copy → drift possible.
286
+ click.echo(
287
+ "warning: coord CLI is running from a non-editable install "
288
+ "(site-packages snapshot) but a source checkout exists at "
289
+ f"{local_init.parent}.\n"
290
+ " Edits to the source tree will NOT reach the CLI. "
291
+ "Fix: pip install -e .",
292
+ err=True,
293
+ )
294
+ except Exception: # noqa: BLE001 — best-effort, never break the CLI
295
+ pass
296
+
297
+
298
+ def _warn_if_editable_checkout_moved() -> None:
299
+ """#561/#601 backstop: when running from an EDITABLE checkout, warn loudly if
300
+ its branch was moved off the default.
301
+
302
+ A Build/`coord test`/smoke that git-checkout'd the base — or an interactive
303
+ agent inspecting a branch in the live checkout — silently puts the running
304
+ coordinator on that branch's code until restored (#561 incident: disabled
305
+ guards; #601 incident: old code + retired local DB). This makes that state
306
+ visible on every command instead of waiting for a verdict or manual restore.
307
+ """
308
+ import subprocess # noqa: PLC0415
309
+ import sys as _sys # noqa: PLC0415
310
+
311
+ if "pytest" in _sys.modules:
312
+ return # don't add startup noise to the test suite
313
+ try:
314
+ import coord as _coord # noqa: PLC0415
315
+
316
+ coord_file = _coord.__file__ or ""
317
+ if "site-packages" in coord_file:
318
+ return # PyPI/snapshot install — moving a checkout can't affect it.
319
+ repo_root = Path(coord_file).resolve().parents[1]
320
+ if not (repo_root / ".git").exists():
321
+ return
322
+ head = subprocess.run(
323
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
324
+ cwd=str(repo_root), capture_output=True, text=True, timeout=3,
325
+ )
326
+ if head.returncode != 0:
327
+ return
328
+ branch = head.stdout.strip()
329
+ if branch in ("main", "master"):
330
+ return
331
+ shown = "(detached HEAD)" if branch == "HEAD" else f"'{branch}'"
332
+ click.echo(
333
+ f"⚠ coord: editable checkout {repo_root} is on {shown}, not the "
334
+ "default branch — the running coordinator is on THAT code. A "
335
+ "Build/smoke/test may have checked it out. Restore with: "
336
+ f"git -C {repo_root} checkout main",
337
+ err=True,
338
+ )
339
+ except Exception: # noqa: BLE001 — best-effort, never break the CLI
340
+ pass
341
+
342
+
343
+ @click.group(help="Multi-agent coordinator for Claude Code workers.")
344
+ @click.version_option(__version__, prog_name="coord")
345
+ def main() -> None:
346
+ """coord — coordinate Claude Code workers across machines and repos."""
347
+ _warn_if_source_install_drift()
348
+ _warn_if_editable_checkout_moved()
349
+
350
+
351
+ # Registration order below matches the historical decoration order in the
352
+ # pre-#747 cli.py exactly.
353
+ main.add_command(version)
354
+ main.add_command(config_cmd)
355
+ main.add_command(init)
356
+ main.add_command(agent)
357
+ main.add_command(status)
358
+ main.add_command(plan)
359
+ main.add_command(approve)
360
+ main.add_command(assign)
361
+ main.add_command(log)
362
+ main.add_command(show_plan)
363
+ main.add_command(inject)
364
+ main.add_command(chat_continue)
365
+ main.add_command(stop)
366
+ main.add_command(report_result)
367
+ main.add_command(verify_merge)
368
+ main.add_command(set_review_findings)
369
+ main.add_command(review_reaffirm)
370
+ main.add_command(retry)
371
+ main.add_command(pull_artifact)
372
+ main.add_command(bounce)
373
+ main.add_command(sync)
374
+ main.add_command(pause)
375
+ main.add_command(unpause)
376
+ main.add_command(refine_chat)
377
+ main.add_command(test_chat)
378
+ main.add_command(new_issue_chat)
379
+ main.add_command(refine_board)
380
+ main.add_command(ready)
381
+ main.add_command(refine)
382
+ main.add_command(reconcile_merges)
383
+ main.add_command(housekeeping)
384
+ main.add_command(diagnose)
385
+ main.add_command(gates)
386
+ main.add_command(doctor)
387
+ main.add_command(health)
388
+ main.add_command(issue_group)
389
+ main.add_command(context_group)
390
+ main.add_command(audit)
391
+ main.add_command(report_group)
392
+ main.add_command(scorecard)
393
+ main.add_command(milestone_group)
394
+ main.add_command(plans_cmd)
395
+ main.add_command(fix_briefing_cmd)
396
+ main.add_command(track)
397
+ main.add_command(untrack)
398
+ main.add_command(backlog)
399
+ main.add_command(queue)
400
+ main.add_command(unqueue)
401
+ main.add_command(set_test_mode)
402
+ main.add_command(notify)
403
+ main.add_command(post_pending_reviews)
404
+ main.add_command(merge)
405
+ main.add_command(resume)
406
+ main.add_command(test)
407
+ main.add_command(test_plan_cmd)
408
+ main.add_command(split)
409
+ main.add_command(done)
410
+ main.add_command(session)
411
+ main.add_command(sessions_cmd)
412
+ main.add_command(reattach)
413
+ main.add_command(terminal_group)
414
+ main.add_command(usage)
415
+ main.add_command(web)
416
+ main.add_command(serve)
417
+ main.add_command(wait)
418
+ main.add_command(watch)
419
+ main.add_command(drive)
420
+ main.add_command(drive_sessions)
421
+ main.add_command(drive_attach)
422
+ main.add_command(drive_stop)
423
+ main.add_command(drive_queue_group)
424
+ main.add_command(escalate_group)
425
+ main.add_command(pr)
426
+ main.add_command(fix)
427
+ main.add_command(review)
428
+ main.add_command(approve_plan)
429
+ main.add_command(reject_plan)
430
+ main.add_command(resume_stuck)
431
+ main.add_command(install_skills)
432
+ main.add_command(acceptance_group)
433
+ # #2063: the Gate-A human sign-off verdict, sibling to `coord test
434
+ # --passed|--fail`. Flat (not under `acceptance`) because it is an operator
435
+ # gesture on a milestone, not part of the acceptance runner.
436
+ main.add_command(gate_a)
437
+ main.add_command(release_preflight)
438
+ # #1834: `coord release verify` (and `coord release preflight` as an alias of
439
+ # the flat command above), grouped so the pre-tag and post-release halves of
440
+ # the release story are discoverable together.
441
+ main.add_command(release_group)
442
+ main.add_command(tui_group)
443
+
444
+
445
+ # #1809: without this guard, `python -m coord.cli <args>` just IMPORTS the
446
+ # module — building every `click.group`/`add_command` above, then returning —
447
+ # and exits 0 having run nothing and printed nothing. That import-only path is
448
+ # exactly what `coord.drive.coord_argv()` falls back to whenever `coord` isn't
449
+ # on PATH (a bare venv, a systemd user unit, a non-interactive ssh session —
450
+ # see #402), so every subprocess the driver or the drive queue spawned on such
451
+ # a host was a silent no-op that reported success. `python -m coord.cli
452
+ # --version` and the fallback argv both need `main()` to actually run.
453
+ if __name__ == "__main__": # pragma: no cover — exercised via subprocess in tests
454
+ main()