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,217 @@
1
+ """``coord gate-a`` — record (or read) the human sign-off on a milestone's
2
+ Gate-A contract (#2063).
3
+
4
+ The sibling gate is enforced; this one was not. ``coord test --passed|--fail``
5
+ writes a verdict the pipeline holds review on
6
+ (``PipelineConfig.test_precedes_review``), whereas Gate A said only "merging
7
+ the Gate-A PR is sign-off" — a convention anything able to merge a PR
8
+ satisfies, including a coordinator session, silently, on CI green. It failed
9
+ on two consecutive coord-portal milestones (ms-1/PR #18, ms-2/PR #35): both
10
+ times the mocks were merged unseen, and the operator raised it themselves.
11
+
12
+ This command is the verdict half of the fix. The refusal half lives in
13
+ :func:`coord.milestone_dispatch.issue_oracle_ready` — deliberately at the
14
+ point where the contract is **consumed**, not at the merge, because the
15
+ Gate-A PR is merged with ``gh pr merge`` outside coord entirely.
16
+
17
+ Usage::
18
+
19
+ coord gate-a acme-portal 17 # read the current verdict
20
+ coord gate-a --approved acme-portal 17
21
+ coord gate-a --changes acme-portal 17 --note "status vocabulary is wrong"
22
+
23
+ The verdict is keyed to the **content hash** of
24
+ ``tests/acceptance/ms-NN/contract.md`` on the repo's default branch, so a
25
+ later ``coord acceptance mock ... --amend`` invalidates it automatically:
26
+ approving v1 must not silently approve v2.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import getpass
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ import click
36
+
37
+ from coord import gate_a as gate_a_mod
38
+ from coord.commands._common import _CONFIG_OPTION, _load_config
39
+
40
+
41
+ def _actor() -> str:
42
+ try:
43
+ return getpass.getuser()
44
+ except Exception: # noqa: BLE001 — no passwd entry in some containers
45
+ return "unknown"
46
+
47
+
48
+ def _fetch_contract(repo_cfg, milestone_number: int) -> str | None:
49
+ from coord import github_ops # noqa: PLC0415
50
+ from coord.acceptance import gate_a_contract_path # noqa: PLC0415
51
+
52
+ try:
53
+ return github_ops.get_repo_file(
54
+ repo_cfg.github,
55
+ gate_a_contract_path(milestone_number),
56
+ branch=repo_cfg.default_branch,
57
+ )
58
+ except RuntimeError:
59
+ return None
60
+
61
+
62
+ @click.command(
63
+ "gate-a",
64
+ help=(
65
+ "Record the human sign-off on a milestone's Gate-A contract, or read "
66
+ "the current verdict. Mirrors `coord test --passed|--fail`: nothing "
67
+ "downstream of Gate A dispatches until a verdict exists for the "
68
+ "contract's CURRENT content (#2063)."
69
+ ),
70
+ )
71
+ @click.argument("repo")
72
+ @click.argument("tracking_issue", type=int)
73
+ @click.option(
74
+ "--approved",
75
+ "verdict",
76
+ flag_value=gate_a_mod.VERDICT_APPROVED,
77
+ help="A human read the rendered mock(s) + contract.md and signs off.",
78
+ )
79
+ @click.option(
80
+ "--changes",
81
+ "verdict",
82
+ flag_value=gate_a_mod.VERDICT_CHANGES,
83
+ help=(
84
+ "A human read it and wants the contract changed — amend it with "
85
+ "`coord acceptance mock <repo> <issue> --amend \"...\"`."
86
+ ),
87
+ )
88
+ @click.option(
89
+ "--note",
90
+ default="",
91
+ help="What you want changed (or any context worth keeping with the verdict).",
92
+ )
93
+ @_CONFIG_OPTION
94
+ def gate_a(
95
+ repo: str,
96
+ tracking_issue: int,
97
+ verdict: str | None,
98
+ note: str,
99
+ config_path: Path,
100
+ ) -> None:
101
+ from coord import state # noqa: PLC0415
102
+ from coord import github_ops # noqa: PLC0415
103
+ from coord.audit import record_audit # noqa: PLC0415
104
+
105
+ cfg = _load_config(config_path)
106
+ repo_cfg = cfg.repo(repo)
107
+ if repo_cfg is None:
108
+ click.echo(f"error: unknown repo {repo!r}", err=True)
109
+ sys.exit(2)
110
+
111
+ try:
112
+ issue_data = github_ops.get_issue(repo_cfg.github, tracking_issue)
113
+ except RuntimeError as e:
114
+ click.echo(f"error: could not fetch #{tracking_issue}: {e}", err=True)
115
+ sys.exit(1)
116
+
117
+ milestone_number = (issue_data.get("milestone") or {}).get("number")
118
+ if milestone_number is None:
119
+ click.echo(
120
+ f"error: #{tracking_issue} has no milestone — Gate A is a "
121
+ "milestone-level gate, so there is nothing to sign off on.",
122
+ err=True,
123
+ )
124
+ sys.exit(2)
125
+ milestone_number = int(milestone_number)
126
+
127
+ contract_text = _fetch_contract(repo_cfg, milestone_number)
128
+ if contract_text is None:
129
+ from coord.acceptance import gate_a_contract_path # noqa: PLC0415
130
+
131
+ click.echo(
132
+ f"error: {gate_a_contract_path(milestone_number)!r} does not exist "
133
+ f"on {repo_cfg.github}@{repo_cfg.default_branch} yet — there is no "
134
+ "contract to approve. Run `coord acceptance mock "
135
+ f"{repo} {tracking_issue}` first, and merge its PR.",
136
+ err=True,
137
+ )
138
+ sys.exit(1)
139
+
140
+ sha = gate_a_mod.contract_digest(contract_text)
141
+
142
+ # Read-only mode: no verdict flag => report where this milestone stands.
143
+ if verdict is None:
144
+ stored = state.get_gate_a_approval(
145
+ repo_name=repo_cfg.name, milestone_number=milestone_number
146
+ )
147
+ decision = gate_a_mod.evaluate(
148
+ repo_name=repo_cfg.name,
149
+ milestone_number=milestone_number,
150
+ contract_text=contract_text,
151
+ approval=stored,
152
+ )
153
+ click.echo(
154
+ f"Gate A ms-{milestone_number} ({repo}#{tracking_issue}): "
155
+ f"{gate_a_mod.summarise(decision)}"
156
+ )
157
+ if decision.approval is not None and decision.approval.note:
158
+ click.echo(f" note: {decision.approval.note}")
159
+ if not decision.ok:
160
+ click.echo("")
161
+ click.echo(decision.reason or "")
162
+ sys.exit(1)
163
+ return
164
+
165
+ record = gate_a_mod.make_record(
166
+ repo_name=repo_cfg.name,
167
+ milestone_number=milestone_number,
168
+ verdict=verdict,
169
+ contract_sha=sha,
170
+ tracking_issue=tracking_issue,
171
+ note=note,
172
+ actor=_actor(),
173
+ )
174
+ try:
175
+ state.save_gate_a_approval(record.to_dict())
176
+ except Exception as e: # noqa: BLE001
177
+ click.echo(f"error: could not record Gate A verdict: {e}", err=True)
178
+ sys.exit(1)
179
+
180
+ record_audit(
181
+ tier="business",
182
+ category="gate",
183
+ event_type=f"gate_a_{verdict}",
184
+ actor=record.actor,
185
+ summary=(
186
+ f"Gate A {verdict} for {repo} ms-{milestone_number} "
187
+ f"(contract {gate_a_mod.short_digest(sha)})"
188
+ + (f": {note}" if note else "")
189
+ ),
190
+ repo=repo_cfg.name,
191
+ issue=tracking_issue,
192
+ details={
193
+ "milestone_number": milestone_number,
194
+ "contract_sha": sha,
195
+ "verdict": verdict,
196
+ "note": note,
197
+ },
198
+ )
199
+
200
+ if verdict == gate_a_mod.VERDICT_APPROVED:
201
+ click.echo(
202
+ f"Gate A approved for {repo} ms-{milestone_number} "
203
+ f"(contract {gate_a_mod.short_digest(sha)}) — this milestone's "
204
+ "issues may now dispatch."
205
+ )
206
+ click.echo(
207
+ " An `--amend` to the contract invalidates this; re-approve after one."
208
+ )
209
+ else:
210
+ click.echo(
211
+ f"Gate A changes requested for {repo} ms-{milestone_number} "
212
+ f"(contract {gate_a_mod.short_digest(sha)}) — dispatch stays refused."
213
+ )
214
+ click.echo(
215
+ f' Next: coord acceptance mock {repo} {tracking_issue} --amend "'
216
+ f'{note or "<what to change>"}"'
217
+ )
@@ -0,0 +1,89 @@
1
+ """`coord gates` — read a work row's gate columns and the live gate decision
2
+ (review / test / merge, including #1479 staleness) for one issue.
3
+
4
+ #1657: previously the only way to see ``test_state``/``smoke_test``/
5
+ ``review_verdict``/etc. was a hand-extracted bearer token plus a raw
6
+ ``/board`` curl, and even that didn't show *why* a gate would refuse (the
7
+ #1479 freshness computation). This command surfaces both from a single,
8
+ read-only, thin-client-reachable call.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import click
17
+
18
+ from coord.commands._common import _CONFIG_OPTION, _load_config
19
+
20
+
21
+ def _gates_via_daemon(svc, params: dict) -> None:
22
+ """Relay ``coord gates`` to the daemon host (where the canonical board +
23
+ gh live), mirroring ``coord diagnose``/``coord reconcile-merges``. The
24
+ #1479 freshness computation needs live ``gh`` lookups that only work
25
+ where ``gh`` is authenticated — routing the whole read there keeps a
26
+ thin client's answer identical to running it on the daemon directly."""
27
+ from coord.client import post_record # noqa: PLC0415
28
+
29
+ try:
30
+ resp = post_record(svc, "/gates", params, timeout=60.0)
31
+ except Exception as exc: # noqa: BLE001
32
+ click.echo(f"error: gates via daemon failed: {exc}", err=True)
33
+ sys.exit(1)
34
+ output = resp.get("output") or ""
35
+ if output:
36
+ click.echo(output, nl=False)
37
+ if resp.get("error"):
38
+ click.echo(f"error: {resp['error']}", err=True)
39
+ code = resp.get("exit_code") or 0
40
+ if code:
41
+ sys.exit(int(code))
42
+
43
+
44
+ @click.command(
45
+ help=(
46
+ "Print a work row's gate columns (test_state, smoke_test, test_reason, "
47
+ "test_toolchain, review_state, review_verdict, review_of_assignment_id), "
48
+ "plus the LIVE review/test/merge gate decision — including whether a recorded verdict "
49
+ "is #1479-stale (recorded against a base/branch SHA that has since "
50
+ "moved) and the SHAs compared. Read-only: never mutates board state."
51
+ )
52
+ )
53
+ @click.argument("repo")
54
+ @click.argument("issue", type=int)
55
+ @click.option(
56
+ "--json",
57
+ "as_json",
58
+ is_flag=True,
59
+ help="Emit the report as a JSON object on stdout instead of the human-readable form.",
60
+ )
61
+ @_CONFIG_OPTION
62
+ def gates(repo: str, issue: int, as_json: bool, config_path: Path) -> None:
63
+ # The #1479 freshness comparison needs live `gh` lookups (branch/base
64
+ # SHAs, patch-id, milestone resolution) — only reliably available where
65
+ # `gh` is authenticated. Route the whole read to the daemon host, same
66
+ # whole-command reroute pattern as `coord diagnose`/`coord
67
+ # reconcile-merges`. COORD_GATES_ON_DAEMON guards the daemon against
68
+ # re-routing to itself.
69
+ from coord.board_service import daemon_reroute_target # noqa: PLC0415
70
+
71
+ _svc = daemon_reroute_target("COORD_GATES_ON_DAEMON")
72
+ if _svc is not None:
73
+ _gates_via_daemon(_svc, {"repo": repo, "issue": issue, "as_json": as_json})
74
+ return
75
+
76
+ import json as _json # noqa: PLC0415
77
+
78
+ from coord import github_ops as gh_ops # noqa: PLC0415
79
+ from coord.gates import build_gate_report, format_gate_report, report_to_dict # noqa: PLC0415
80
+ from coord.state import build_board # noqa: PLC0415
81
+
82
+ cfg = _load_config(config_path)
83
+ board = build_board()
84
+ report = build_gate_report(board, cfg, repo, issue, gh_ops=gh_ops)
85
+
86
+ if as_json:
87
+ click.echo(_json.dumps(report_to_dict(report), indent=2))
88
+ else:
89
+ click.echo(format_gate_report(report))