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,616 @@
1
+ """``coord drive`` — the unattended single-issue driver (#1392).
2
+
3
+ Thin Click wrapper: parse flags into :class:`~coord.drive.DriveOptions`, build
4
+ the :class:`~coord.drive.Driver`, translate :class:`~coord.drive.DriveError`
5
+ into a clean exit code. Every decision lives in ``coord/drive.py``.
6
+
7
+ Replaces ``scripts/drive-issue.sh`` + ``scripts/coord_issue_state.py``, deleted
8
+ in the same change (no two implementations). ``scripts/coord-test-runner.sh``
9
+ deliberately stays as shell — it is genuinely subprocess/venv/cargo-env work,
10
+ and its four sharp-edged parsers already live in ``coord/test_report.py``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ import click
21
+
22
+ from coord.commands._common import _CONFIG_OPTION, _load_config
23
+
24
+
25
+ _DRIVE_HELP = """Drive ONE issue Work → Test → Review → Merge, unattended.
26
+
27
+ A resumable state machine over the daemon's board: it dispatches the WORK
28
+ assignment, then OBSERVES Test/Review/Merge (coord dispatches all three
29
+ itself), looping a failing test through `coord fix` on the same branch.
30
+ Re-running it on the same issue is safe and resumes from wherever the board
31
+ actually is.
32
+
33
+ #1453: when this issue resolves to a milestone with a merged Gate-A contract
34
+ (`tests/acceptance/ms-NN/contract.md`) and the repo has an acceptance driver
35
+ configured, the sealed JIT acceptance slice is authored first (`coord
36
+ acceptance author <repo> <tracking_issue> --issue <N>`) and observed through
37
+ to a landed merge — ONLY THEN is `coord assign`/`coord approve-plan` run.
38
+ Pass --no-acceptance to skip this and drive the issue exactly as before.
39
+ The preflight banner's "acceptance" line always states which mode a run is
40
+ in and why.
41
+
42
+ Exit codes: 0 merged (verified against the remote default branch), or review
43
+ approved with --no-merge; 1 a stage reached a terminal failure a script cannot
44
+ resolve; 2 bad usage / configuration; 3 deadline exceeded.
45
+ """
46
+
47
+
48
+ def _rebuild_drive_argv(
49
+ repo: str,
50
+ issue: int,
51
+ *,
52
+ machine: str,
53
+ model: str,
54
+ briefing_file: str,
55
+ do_plan: bool,
56
+ max_fix_rounds: int,
57
+ skip_test: bool,
58
+ repo_path: str,
59
+ poll: float,
60
+ max_work_retries: int,
61
+ deadline_mins: float,
62
+ stall_mins: float,
63
+ notify: bool,
64
+ accept_advisory: bool,
65
+ force_review: bool,
66
+ no_merge: bool,
67
+ merge_method: str,
68
+ max_merge_attempts: int,
69
+ dry_run: bool,
70
+ config_path: Path,
71
+ ) -> list[str]:
72
+ """Rebuild a ``drive <repo> <issue> ...`` argv from parsed flags (`--tmux`).
73
+
74
+ The `--tmux` launch re-execs `coord drive` (minus `--tmux`) detached
75
+ inside a fresh tmux session, so every OTHER flag the operator passed
76
+ must survive the trip. Deliberately always emits every option (rather
77
+ than only the ones that differ from Click's own default) so this stays
78
+ correct even if a default ever changes — no drift between two places
79
+ that both need to know it.
80
+ """
81
+ argv = ["drive", repo, str(issue)]
82
+ if machine:
83
+ argv += ["--machine", machine]
84
+ if model:
85
+ argv += ["--model", model]
86
+ if briefing_file:
87
+ argv += ["--briefing-file", briefing_file]
88
+ if do_plan:
89
+ argv.append("--plan")
90
+ argv += ["--max-fix-rounds", str(max_fix_rounds)]
91
+ if skip_test:
92
+ argv.append("--skip-test")
93
+ if repo_path:
94
+ argv += ["--repo-path", repo_path]
95
+ argv += ["--poll", str(poll)]
96
+ argv += ["--max-work-retries", str(max_work_retries)]
97
+ argv += ["--deadline", str(deadline_mins)]
98
+ argv += ["--stall", str(stall_mins)]
99
+ if notify:
100
+ argv.append("--notify")
101
+ if accept_advisory:
102
+ argv.append("--accept-advisory")
103
+ if force_review:
104
+ argv.append("--force-review")
105
+ if no_merge:
106
+ argv.append("--no-merge")
107
+ argv += ["--merge-method", merge_method]
108
+ argv += ["--max-merge-attempts", str(max_merge_attempts)]
109
+ if dry_run:
110
+ argv.append("--dry-run")
111
+ if config_path:
112
+ argv += ["--config", str(config_path)]
113
+ return argv
114
+
115
+
116
+ @click.command("drive", help=_DRIVE_HELP)
117
+ @click.argument("repo")
118
+ @click.argument("issue", type=int)
119
+ @click.option(
120
+ "--machine",
121
+ default="",
122
+ help=(
123
+ "Machine for the work dispatch (default: least-loaded unpaused machine "
124
+ "that hosts the repo)."
125
+ ),
126
+ )
127
+ @click.option(
128
+ "--model",
129
+ default="",
130
+ help="Model tier (haiku|sonnet|opus). Default: models.default.",
131
+ )
132
+ @click.option(
133
+ "--briefing-file",
134
+ default="",
135
+ type=click.Path(),
136
+ help=(
137
+ "REPLACES the entire auto-generated briefing for the work dispatch — it "
138
+ "is NOT an addendum. The issue body, project rules and file scope are "
139
+ "all dropped, so the worker only ever sees this file. To ADD guidance "
140
+ "while keeping the real briefing, use `coord context add --pin <repo> "
141
+ "<issue> '<note>'`, which prepends to every briefing (#603)."
142
+ ),
143
+ )
144
+ @click.option(
145
+ "--plan",
146
+ "do_plan",
147
+ is_flag=True,
148
+ help=(
149
+ "Run a read-only plan stage first and auto-approve it "
150
+ "(coord assign --plan-only → coord approve-plan)."
151
+ ),
152
+ )
153
+ @click.option(
154
+ "--max-fix-rounds",
155
+ default=3,
156
+ show_default=True,
157
+ help=(
158
+ "Headless `coord fix` rounds this run is willing to spend, shared by "
159
+ "BOTH fix arms: a failing test suite and a request-changes review "
160
+ "(#1692). Each round continues the SAME branch with the model "
161
+ "escalated (sonnet → opus). coord dispatches the Test stage itself "
162
+ "onto a capability-matched machine (#1426); this only observes the "
163
+ "verdict. Distinct from pipeline.max_review_iterations, which bounds "
164
+ "the review loop per ISSUE across every drive."
165
+ ),
166
+ )
167
+ @click.option(
168
+ "--skip-test",
169
+ is_flag=True,
170
+ help=(
171
+ "Record the Test gate as `skipped` directly (via `coord test --skipped`) "
172
+ "— no dispatch. Use only for genuinely untestable diffs."
173
+ ),
174
+ )
175
+ @click.option(
176
+ "--repo-path",
177
+ default="",
178
+ type=click.Path(),
179
+ help=(
180
+ "Local checkout used for branch/merge verification (git fetch + "
181
+ "cherry against origin). Default: ~/src/<repo>."
182
+ ),
183
+ )
184
+ @click.option("--poll", default=60.0, show_default=True, help="Board poll interval (s).")
185
+ @click.option(
186
+ "--max-work-retries",
187
+ default=1,
188
+ show_default=True,
189
+ help="`coord retry` attempts on a failed work stage.",
190
+ )
191
+ @click.option(
192
+ "--deadline",
193
+ "deadline_mins",
194
+ default=240.0,
195
+ show_default=True,
196
+ help="Give up after this long (minutes).",
197
+ )
198
+ @click.option(
199
+ "--stall",
200
+ "stall_mins",
201
+ default=20.0,
202
+ show_default=True,
203
+ help=(
204
+ "Warn (and nudge, with --notify) after this long with no state "
205
+ "change, repeating on the same cadence for as long as the stall "
206
+ "continues."
207
+ ),
208
+ )
209
+ @click.option(
210
+ "--notify",
211
+ is_flag=True,
212
+ help=(
213
+ "Also run `coord notify` under ~/.coord/notify.lock when stalled, to cut "
214
+ "the 5-minute timer latency. OFF by default: two drivers racing to "
215
+ "dispatch the same thing is the #476/#477 duplicate-fix-worker incident."
216
+ ),
217
+ )
218
+ @click.option(
219
+ "--accept-advisory",
220
+ is_flag=True,
221
+ help=(
222
+ "Proceed when the work row is ADVISORY but its branch demonstrably "
223
+ "carries commits. Needed while #1357 is open."
224
+ ),
225
+ )
226
+ @click.option(
227
+ "--force-review",
228
+ is_flag=True,
229
+ help=(
230
+ "Explicitly request the review for work completed in an INTERACTIVE "
231
+ "session. coord's #555 guard never auto-dispatches one for "
232
+ "provider=claude-pty, so without this the run stops at preflight "
233
+ "rather than stalling forever."
234
+ ),
235
+ )
236
+ @click.option("--no-merge", is_flag=True, help="Stop after the review approves.")
237
+ @click.option(
238
+ "--merge-method",
239
+ type=click.Choice(["rebase", "squash", "merge"]),
240
+ default="rebase",
241
+ show_default=True,
242
+ )
243
+ @click.option(
244
+ "--max-merge-attempts",
245
+ default=3,
246
+ show_default=True,
247
+ help="`coord merge` attempts before giving up (bounds a merge the board never reflects).",
248
+ )
249
+ @click.option(
250
+ "--dry-run",
251
+ is_flag=True,
252
+ help="Print the resolved plan and current state, then exit.",
253
+ )
254
+ @click.option(
255
+ "--tmux",
256
+ "use_tmux",
257
+ is_flag=True,
258
+ help=(
259
+ "Launch the drive loop DETACHED in a `coord-drive-<repo>-<issue>` tmux "
260
+ "session instead of running inline (#1398). Waits a few seconds to "
261
+ "confirm the session is still alive and has started writing its run "
262
+ "log (#1606) before exiting 0 — a launch that dies immediately (e.g. "
263
+ "nothing left to do) exits non-zero with the reason instead of "
264
+ "printing a false success banner. Once confirmed live, this "
265
+ "invocation exits immediately — the run survives this terminal "
266
+ "closing, a TUI restart, or an ssh drop. Reattach with `coord "
267
+ "drive-attach <repo> <issue>`; list live runs with `coord "
268
+ "drive-sessions`; stop with `coord drive-stop <repo> <issue>` — killing "
269
+ "the session releases the per-issue flock, which IS the correct Stop."
270
+ ),
271
+ )
272
+ @click.option(
273
+ "--no-acceptance",
274
+ is_flag=True,
275
+ help=(
276
+ "Skip the #1453 oracle-loop JIT slice authoring step even when this "
277
+ "issue's milestone has a merged Gate-A contract — use when the "
278
+ "contract is stale/wrong for this issue, or you just want a plain "
279
+ "run. Dispatches straight to `coord assign` as before #1453."
280
+ ),
281
+ )
282
+ @_CONFIG_OPTION
283
+ def drive(
284
+ repo: str,
285
+ issue: int,
286
+ machine: str,
287
+ model: str,
288
+ briefing_file: str,
289
+ do_plan: bool,
290
+ max_fix_rounds: int,
291
+ skip_test: bool,
292
+ repo_path: str,
293
+ poll: float,
294
+ max_work_retries: int,
295
+ deadline_mins: float,
296
+ stall_mins: float,
297
+ notify: bool,
298
+ accept_advisory: bool,
299
+ force_review: bool,
300
+ no_merge: bool,
301
+ merge_method: str,
302
+ max_merge_attempts: int,
303
+ dry_run: bool,
304
+ use_tmux: bool,
305
+ no_acceptance: bool,
306
+ config_path: Path,
307
+ ) -> None:
308
+ # Imported lazily so `coord --help` doesn't pay for httpx/subprocess setup.
309
+ from coord.drive import ( # noqa: PLC0415
310
+ Driver,
311
+ DriveError,
312
+ DriveOptions,
313
+ coord_argv,
314
+ launch_drive_in_tmux,
315
+ )
316
+
317
+ if use_tmux:
318
+ argv = coord_argv() + _rebuild_drive_argv(
319
+ repo,
320
+ issue,
321
+ machine=machine,
322
+ model=model,
323
+ briefing_file=briefing_file,
324
+ do_plan=do_plan,
325
+ max_fix_rounds=max_fix_rounds,
326
+ skip_test=skip_test,
327
+ repo_path=repo_path,
328
+ poll=poll,
329
+ max_work_retries=max_work_retries,
330
+ deadline_mins=deadline_mins,
331
+ stall_mins=stall_mins,
332
+ notify=notify,
333
+ accept_advisory=accept_advisory,
334
+ force_review=force_review,
335
+ no_merge=no_merge,
336
+ merge_method=merge_method,
337
+ max_merge_attempts=max_merge_attempts,
338
+ dry_run=dry_run,
339
+ config_path=config_path,
340
+ )
341
+ try:
342
+ session = launch_drive_in_tmux(argv, repo=repo, issue=issue)
343
+ except DriveError as exc:
344
+ click.echo(f"✗ {exc}", err=True)
345
+ raise SystemExit(exc.exit_code) from None
346
+ click.echo(f"driving {repo} #{issue} in tmux session {session!r}")
347
+ click.echo(f" attach with: coord drive-attach {repo} {issue}")
348
+ click.echo(f" stop with: coord drive-stop {repo} {issue}")
349
+ raise SystemExit(0)
350
+
351
+ config = _load_config(config_path)
352
+ opts = DriveOptions(
353
+ machine=machine,
354
+ model=model,
355
+ briefing_file=briefing_file,
356
+ do_plan=do_plan,
357
+ max_fix_rounds=max_fix_rounds,
358
+ skip_test=skip_test,
359
+ repo_path=repo_path,
360
+ poll=poll,
361
+ max_work_retries=max_work_retries,
362
+ deadline_mins=deadline_mins,
363
+ stall_mins=stall_mins,
364
+ notify=notify,
365
+ do_merge=not no_merge,
366
+ merge_method=merge_method,
367
+ accept_advisory=accept_advisory,
368
+ force_review=force_review,
369
+ dry_run=dry_run,
370
+ max_merge_attempts=max_merge_attempts,
371
+ no_acceptance=no_acceptance,
372
+ config_path=str(config_path) if config_path else "",
373
+ )
374
+ driver = Driver(repo=repo, issue=issue, opts=opts, config=config)
375
+ try:
376
+ code = driver.run()
377
+ except DriveError as exc:
378
+ click.echo(f"✗ {exc}", err=True)
379
+ raise SystemExit(exc.exit_code) from None
380
+ except KeyboardInterrupt:
381
+ click.echo("interrupted", err=True)
382
+ raise SystemExit(130) from None
383
+ raise SystemExit(code)
384
+
385
+
386
+ # ── `--tmux` companions: list / attach / stop (#1398) ─────────────────────────
387
+ #
388
+ # Three flat, hyphenated commands rather than a `drive` sub-group — `drive`
389
+ # itself already spends its position on `REPO ISSUE`, so a group would need
390
+ # a distinct verb anyway. Mirrors the naming of other hyphenated one-off
391
+ # commands in this CLI (`verify-merge`, `fix-briefing`, `approve-plan`).
392
+
393
+
394
+ @click.command(
395
+ "drive-sessions",
396
+ help="List live `coord drive --tmux` sessions (coord-drive-<repo>-<issue>).",
397
+ )
398
+ @click.option(
399
+ "--json", "output_json", is_flag=True, default=False,
400
+ help="Output as JSON — an array of {repo, issue, session_name, attached}. "
401
+ "Consumed by coord-tui to badge Pipeline rows and gate the menu.",
402
+ )
403
+ def drive_sessions(output_json: bool) -> None:
404
+ import json as _json # noqa: PLC0415
405
+
406
+ from coord.drive import list_drive_sessions # noqa: PLC0415
407
+
408
+ entries = list_drive_sessions()
409
+ if output_json:
410
+ click.echo(_json.dumps(entries))
411
+ return
412
+ if not entries:
413
+ click.echo("No live drive sessions.")
414
+ return
415
+ for e in entries:
416
+ attached_tag = " [attached]" if e.get("attached") else ""
417
+ click.echo(f" {e['repo']} #{e['issue']}{attached_tag}")
418
+ click.echo(f" attach with: coord drive-attach {e['repo']} {e['issue']}")
419
+ click.echo(f" stop with: coord drive-stop {e['repo']} {e['issue']}")
420
+
421
+
422
+ @click.command(
423
+ "drive-attach",
424
+ help=(
425
+ "Attach to a live `coord drive --tmux` session for REPO ISSUE. "
426
+ "Type this into a local PTY exactly as `coord reattach`/`coord "
427
+ "terminal attach` are used for other session kinds."
428
+ ),
429
+ )
430
+ @click.argument("repo")
431
+ @click.argument("issue", type=int)
432
+ def drive_attach(repo: str, issue: int) -> None:
433
+ from coord.drive import drive_session_name # noqa: PLC0415
434
+ from coord.interactive import TmuxHost, tmux_session_alive # noqa: PLC0415
435
+
436
+ host = TmuxHost(None)
437
+ session = drive_session_name(repo, issue)
438
+ if not tmux_session_alive(session, host=host):
439
+ click.echo(f"error: no live drive session for {repo} #{issue}.", err=True)
440
+ sys.exit(1)
441
+
442
+ if os.environ.get("TMUX"):
443
+ # Already inside a tmux client — attach-session refuses to nest;
444
+ # switch-client moves the current client into the target session.
445
+ cmd = ["tmux", "switch-client", "-t", session]
446
+ else:
447
+ cmd = list(host.cmd(["attach-session", "-t", session], tty=True))
448
+
449
+ try:
450
+ result = subprocess.run(cmd)
451
+ except (subprocess.SubprocessError, OSError) as exc:
452
+ click.echo(f"error: failed to attach: {exc}", err=True)
453
+ sys.exit(1)
454
+ sys.exit(result.returncode)
455
+
456
+
457
+ @click.command(
458
+ "drive-stop",
459
+ help=(
460
+ "Stop a live `coord drive --tmux` run for REPO ISSUE by killing its "
461
+ "tmux session. This IS the correct cancellation: the per-issue flock "
462
+ "releases the instant the process exits, so the driver's own "
463
+ "already-driving guard never sees a stale lock."
464
+ ),
465
+ )
466
+ @click.argument("repo")
467
+ @click.argument("issue", type=int)
468
+ def drive_stop(repo: str, issue: int) -> None:
469
+ from coord.drive import drive_session_name # noqa: PLC0415
470
+ from coord.interactive import TmuxHost, tmux_session_alive # noqa: PLC0415
471
+
472
+ host = TmuxHost(None)
473
+ session = drive_session_name(repo, issue)
474
+ if not tmux_session_alive(session, host=host):
475
+ click.echo(f"error: no live drive session for {repo} #{issue}.", err=True)
476
+ sys.exit(1)
477
+
478
+ try:
479
+ result = subprocess.run(
480
+ host.cmd(["kill-session", "-t", session]),
481
+ capture_output=True,
482
+ text=True,
483
+ timeout=10.0,
484
+ )
485
+ except (subprocess.SubprocessError, OSError) as exc:
486
+ click.echo(f"error: failed to kill tmux session: {exc}", err=True)
487
+ sys.exit(1)
488
+ if result.returncode != 0:
489
+ click.echo(
490
+ f"error: tmux kill-session failed: {(result.stderr or '').strip()}",
491
+ err=True,
492
+ )
493
+ sys.exit(1)
494
+ click.echo(f"Stopped driving {repo} #{issue}.")
495
+
496
+
497
+ # ── driver escalation records (#1505) ────────────────────────────────────────
498
+ #
499
+ # `_decide_merge` writes one of these the moment the merge stage hits a
500
+ # status no amount of retrying can fix (NEEDS_ATTENTION / an unrecognised
501
+ # status) — see that function's docstring. `record` is the driver's own
502
+ # write (`Driver.run` executes it as the last thing before exiting, via
503
+ # `run_coord`, never as a direct DB call — same CLI-is-the-contract rule
504
+ # every other board mutation in this module follows). `run`/`dismiss`/`list`
505
+ # are the human's one-key responses, surfaced in coord-tui's Pipeline
506
+ # right-click menu (`run-escalation`/`dismiss-escalation`) and reachable
507
+ # from any shell.
508
+
509
+
510
+ @click.group("escalate")
511
+ def escalate_group() -> None:
512
+ """Board-visible "driver stuck" records the merge stage of `coord drive`
513
+ writes instead of burning the retry budget on a status retrying can't
514
+ fix (#1505). `record` is written by the driver itself; the human answers
515
+ with `run` (execute the proposed fix), `dismiss` (clear the record), or
516
+ `list` (see what's outstanding).
517
+ """
518
+
519
+
520
+ @escalate_group.command("record")
521
+ @click.argument("repo")
522
+ @click.argument("issue", type=int)
523
+ @click.option("--stage", default="merge", show_default=True, help="Pipeline stage that escalated.")
524
+ @click.option("--reason", required=True, help="Why the driver stopped.")
525
+ @click.option(
526
+ "--gate", "gates", multiple=True,
527
+ help="One observed gate reading as key=value. Repeatable.",
528
+ )
529
+ @click.option("--command", "proposed_command", required=True, help="The proposed fix, as a shell command.")
530
+ @click.option("--assignment", "assignment_id", default=None, help="The merge-queue/work assignment id, if known.")
531
+ @_CONFIG_OPTION
532
+ def escalate_record(
533
+ repo: str,
534
+ issue: int,
535
+ stage: str,
536
+ reason: str,
537
+ gates: tuple[str, ...],
538
+ proposed_command: str,
539
+ assignment_id: str | None,
540
+ config_path: Path,
541
+ ) -> None:
542
+ """Write (or replace) the escalation record for REPO ISSUE."""
543
+ from coord.state import record_drive_escalation # noqa: PLC0415
544
+
545
+ record_drive_escalation(
546
+ repo,
547
+ issue,
548
+ stage=stage,
549
+ reason=reason,
550
+ gate_readings=" | ".join(gates),
551
+ proposed_command=proposed_command,
552
+ assignment_id=assignment_id,
553
+ )
554
+ click.echo(f"escalation recorded for {repo} #{issue}: {reason}")
555
+
556
+
557
+ @escalate_group.command("dismiss")
558
+ @click.argument("repo")
559
+ @click.argument("issue", type=int)
560
+ @_CONFIG_OPTION
561
+ def escalate_dismiss(repo: str, issue: int, config_path: Path) -> None:
562
+ """Clear the escalation record for REPO ISSUE without acting on it."""
563
+ from coord.state import dismiss_drive_escalation # noqa: PLC0415
564
+
565
+ ok = dismiss_drive_escalation(repo, issue)
566
+ click.echo(f"dismissed escalation for {repo} #{issue}" if ok else "no escalation on file")
567
+
568
+
569
+ @escalate_group.command("list")
570
+ @click.option("--repo", "repo", default=None, help="Restrict to one repo (default: every repo).")
571
+ @_CONFIG_OPTION
572
+ def escalate_list(repo: str | None, config_path: Path) -> None:
573
+ """List every open escalation record."""
574
+ from coord.state import list_drive_escalations # noqa: PLC0415
575
+
576
+ entries = list_drive_escalations(repo)
577
+ if not entries:
578
+ click.echo("(no open escalations)")
579
+ return
580
+ for e in entries:
581
+ click.echo(f"{e['repo_name']} #{e['issue_number']} [{e['stage']}]: {e['reason']}")
582
+ if e.get("gate_readings"):
583
+ click.echo(f" gates: {e['gate_readings']}")
584
+ click.echo(f" proposed: {e['proposed_command']}")
585
+
586
+
587
+ @escalate_group.command("run")
588
+ @click.argument("repo")
589
+ @click.argument("issue", type=int)
590
+ @click.option(
591
+ "--dismiss/--no-dismiss", default=True,
592
+ help="Clear the record after the proposed command exits 0 (default: yes).",
593
+ )
594
+ @_CONFIG_OPTION
595
+ def escalate_run(repo: str, issue: int, dismiss: bool, config_path: Path) -> None:
596
+ """Run the proposed fix command for REPO ISSUE's escalation.
597
+
598
+ This IS the "one-key human decision" the escalation record exists for —
599
+ nothing runs it automatically; a human (or the TUI's "Run it" menu item,
600
+ which shells out to exactly this) has to explicitly ask for it.
601
+ """
602
+ from coord.state import dismiss_drive_escalation, get_drive_escalation # noqa: PLC0415
603
+
604
+ entry = get_drive_escalation(repo, issue)
605
+ if entry is None:
606
+ raise click.ClickException(f"no escalation on file for {repo} #{issue}")
607
+ command = (entry.get("proposed_command") or "").strip()
608
+ if not command:
609
+ raise click.ClickException("escalation record has no proposed command")
610
+ click.echo(f"running: {command}")
611
+ result = subprocess.run(command, shell=True) # noqa: S602 — operator-approved, one-key by design
612
+ if result.returncode != 0:
613
+ raise click.ClickException(f"proposed command exited {result.returncode}")
614
+ if dismiss:
615
+ dismiss_drive_escalation(repo, issue)
616
+ click.echo("done")