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/mock_author.py ADDED
@@ -0,0 +1,334 @@
1
+ """#930 (docs/ORACLE_LOOP.md, Gate A): dispatch a `type="mock-author"` agent.
2
+
3
+ Gate A is the milestone's pre-work architecture gate: before any of a
4
+ milestone's issues may dispatch, an independent agent — zero shared context
5
+ with the eventual workers, mirroring the adversarial code reviewer — renders
6
+ a viewable mock of the milestone's user-facing surface and pins the exact
7
+ black-box contract (CLI names, key screen text, API field shapes) that both
8
+ the workers and the independent test-author (#931) implement/test to.
9
+
10
+ Used by `coord acceptance mock <repo> <tracking_issue>` (the CLI command in
11
+ ``coord/commands/acceptance.py``). Deliberately keyed by the milestone's
12
+ tracking-issue number, not a `--milestone NN` flag — every sibling milestone
13
+ command (`coord milestone order/dispatch/chat/gate-c`) takes
14
+ `<repo> <tracking_issue>`, and the milestone number itself is resolved from
15
+ the tracking issue, same as `coord/milestone_chat.py` already does. The
16
+ on-disk artifact directory is still `tests/acceptance/ms-<milestone_number>/`
17
+ per ORACLE_LOOP.md's settled layout.
18
+
19
+ The mock-author gets a real git worktree + branch (unlike the read-only
20
+ `milestone-chat` type) since its whole job is committing files under
21
+ `tests/acceptance/ms-NN/` — see `type="mock-author"` handling in
22
+ ``coord/agent.py`` (``MOCK_AUTHOR_SYSTEM_PROMPT`` / ``WRITE_CAPABLE_SPEC_
23
+ TYPES``) and the matching exemption from the acceptance-dir auto-forbid in
24
+ ``coord/dispatch.py``. It dispatches through the same
25
+ Work → Test → Review → Merge pipeline as any other branch (`required_gates`
26
+ is the repo's normal `default_gates`) — Gate A produces a normal reviewed
27
+ commit, not a special-cased one.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import uuid
32
+
33
+ import httpx
34
+
35
+ from coord import github_ops
36
+ from coord.acceptance import ms_dirname
37
+ from coord.claim import claim_message, find_work_claim
38
+ from coord.config import Config
39
+ from coord.milestone_chat import _fetch_milestone_issues
40
+ from coord.milestone_dispatch import pick_machine
41
+ from coord.models import Machine, Proposal
42
+
43
+
44
+ def build_mock_author_briefing(
45
+ *,
46
+ repo_slug: str,
47
+ milestone_title: str,
48
+ milestone_number: int,
49
+ tracking_issue_number: int,
50
+ tracking_issue_body: str,
51
+ issues: list[dict],
52
+ driver_kind: str,
53
+ driver_mock_glob: str,
54
+ ) -> str:
55
+ """Compose the seed briefing the mock-author sees as its first user
56
+ message: milestone context + exactly where its output must land."""
57
+ ms_dir = f"tests/acceptance/{ms_dirname(milestone_number)}"
58
+
59
+ parts: list[str] = []
60
+ parts.append(
61
+ f"=== Gate A mock-author context for {repo_slug} "
62
+ f"— milestone {milestone_title!r} ===\n"
63
+ )
64
+
65
+ parts.append(f"TRACKING ISSUE: #{tracking_issue_number}")
66
+ parts.append("MILESTONE TRACKING ISSUE BODY:")
67
+ parts.append(tracking_issue_body.strip() if tracking_issue_body.strip() else "(empty)")
68
+ parts.append("")
69
+
70
+ parts.append(f"OPEN ISSUES UNDER THIS MILESTONE ({len(issues)}):")
71
+ if issues:
72
+ for issue in issues:
73
+ parts.append(f"--- #{issue['number']}: {issue['title']} ---")
74
+ parts.append(issue["body"] if issue["body"] else "(no body)")
75
+ parts.append("")
76
+ else:
77
+ parts.append(" (none fetched)")
78
+
79
+ parts.append("---")
80
+ parts.append(
81
+ f"This repo's acceptance driver is `{driver_kind}` — render the "
82
+ f"mock(s) in that medium as `{driver_mock_glob}` fixtures under "
83
+ f"`{ms_dir}/mocks/`, then write the black-box contract to "
84
+ f"`{ms_dir}/contract.md` (docs/ORACLE_LOOP.md \"Layout\"). If "
85
+ f"`{ms_dir}/contract.md` already exists, you are AMENDING Gate A, "
86
+ "not authoring it from scratch — read it first and edit in place. "
87
+ "Commit and push both to this branch when done."
88
+ )
89
+ return "\n".join(parts)
90
+
91
+
92
+ def build_mock_author_amend_briefing(
93
+ *,
94
+ repo_slug: str,
95
+ milestone_title: str,
96
+ milestone_number: int,
97
+ tracking_issue_number: int,
98
+ amend_text: str,
99
+ ) -> str:
100
+ """#1315: seed briefing for a *targeted* Gate-A contract amendment.
101
+
102
+ Unlike :func:`build_mock_author_briefing` (a full fresh render from the
103
+ milestone's open-issues digest), this skips re-fetching milestone
104
+ context entirely and hands the mock-author exactly the operator's own
105
+ description of what to correct in the already-merged
106
+ ``tests/acceptance/ms-NN/contract.md`` (and/or its ``mocks/``). This is
107
+ the "properly-typed contract-amend dispatch" #1315 closes the gap for:
108
+ before this, the only way to land a small, targeted correction to a
109
+ merged Gate-A contract was a plain ``coord assign`` (defaulting to
110
+ ``type="work"``) — the root cause of #1314's three-way breakage. Still
111
+ dispatches as ``type="mock-author"`` (independently-typed, exempt from
112
+ the ``tests/acceptance/**`` sealing guard — see ``coord/dispatch.py``
113
+ and #1315's ``_sealed_write_guard_tools`` in ``coord/agent.py``), so a
114
+ contract correction never needs to fall back to ``type="work"`` again.
115
+ """
116
+ ms_dir = f"tests/acceptance/{ms_dirname(milestone_number)}"
117
+
118
+ parts: list[str] = []
119
+ parts.append(
120
+ f"=== Gate A CONTRACT AMENDMENT for {repo_slug} — milestone "
121
+ f"{milestone_title!r} (tracking #{tracking_issue_number}) ===\n"
122
+ )
123
+ parts.append(
124
+ f"This is a targeted correction to the ALREADY-MERGED Gate-A "
125
+ f"contract at `{ms_dir}/contract.md` (and/or its mocks under "
126
+ f"`{ms_dir}/mocks/`) — NOT a from-scratch render. Read the existing "
127
+ f"`{ms_dir}/contract.md` first, then apply exactly the correction "
128
+ "described below. Do not regenerate the document from scratch, and "
129
+ f"do not touch any file outside `{ms_dir}/`. Commit and push both "
130
+ "to this branch when done."
131
+ )
132
+ parts.append("")
133
+ parts.append("--- Requested correction ---")
134
+ parts.append(amend_text.strip())
135
+ return "\n".join(parts)
136
+
137
+
138
+ def dispatch_acceptance_mock(
139
+ repo_name: str,
140
+ tracking_issue_number: int,
141
+ config: Config,
142
+ *,
143
+ machine_override: str | None = None,
144
+ path: str | None = None,
145
+ amend_briefing: str | None = None,
146
+ ) -> tuple[str, str]:
147
+ """End-to-end: resolve the milestone, pick a machine, seed the
148
+ briefing, dispatch a ``type="mock-author"`` assignment.
149
+
150
+ *path* (#1125, repo-root-relative, e.g. ``"coord/foo.py"``) resolves
151
+ which driver to use when the repo's acceptance config is routed
152
+ (``acceptance.drivers.<repo>.routes``) — pass the milestone's
153
+ representative subtree (see `AcceptanceConfig.driver_for` for the
154
+ single-path-per-call resolution rule). Unused when the repo has a flat,
155
+ unrouted driver.
156
+
157
+ *amend_briefing* (#1315): when given, skip the full fresh-render
158
+ briefing (:func:`build_mock_author_briefing`, which re-fetches every
159
+ open issue under the milestone) in favor of a narrow, targeted
160
+ amendment briefing (:func:`build_mock_author_amend_briefing`) built
161
+ from this exact text — the "small, targeted fix to an already-merged
162
+ contract" case #1314 hit with no properly-typed tool for it. Also
163
+ leaves ``target_branch`` unset (unlike the fresh-render path's
164
+ ``ms-{N}-gate-a``) so the worker gets a normal auto-named branch off the
165
+ milestone's current base — the original gate-a branch was already
166
+ merged (and likely deleted) by the time an amendment is needed.
167
+
168
+ Returns ``(assignment_id, machine_name)``. Raises ``RuntimeError`` when
169
+ the repo is unknown, has no acceptance driver configured (or, for a
170
+ routed repo, no driver resolves for *path*), the tracking issue can't be
171
+ fetched or has no milestone, the milestone's Gate A is already claimed,
172
+ no machine claims the repo, or the agent rejects the dispatch.
173
+ """
174
+ repo_cfg = config.repo(repo_name)
175
+ if repo_cfg is None:
176
+ raise RuntimeError(f"repo {repo_name!r} not in coordinator.yml")
177
+
178
+ driver_cfg = config.acceptance.driver_for(repo_name, path)
179
+ if driver_cfg is None:
180
+ if config.acceptance.has_driver(repo_name):
181
+ raise RuntimeError(
182
+ f"repo {repo_name!r} has a routed acceptance driver "
183
+ "(acceptance.drivers routes) but no route matched — pass "
184
+ "--for-path to select the milestone's subtree (e.g. "
185
+ "'coord/**')"
186
+ )
187
+ raise RuntimeError(
188
+ f"repo {repo_name!r} has no acceptance driver configured — add "
189
+ "it under acceptance.drivers in coordinator.yml before running "
190
+ "Gate A (docs/ORACLE_LOOP.md)"
191
+ )
192
+
193
+ try:
194
+ issue_data = github_ops.get_issue(repo_cfg.github, tracking_issue_number)
195
+ except RuntimeError as e:
196
+ raise RuntimeError(f"could not fetch #{tracking_issue_number}: {e}") from e
197
+
198
+ milestone = issue_data.get("milestone") or {}
199
+ milestone_number = milestone.get("number")
200
+ if milestone_number is None:
201
+ raise RuntimeError(f"#{tracking_issue_number} has no milestone")
202
+ milestone_title = milestone.get("title") or f"#{milestone_number}"
203
+
204
+ from coord import board_service # noqa: PLC0415
205
+
206
+ board = board_service.read_board()
207
+
208
+ claim = find_work_claim(tracking_issue_number, repo_name, repo_cfg.github, board)
209
+ if claim is not None:
210
+ # #1059: make the refusal actionable. The operator's "PERMANENTLY
211
+ # STUCK" report (#1041) was a stale claim they "found no way to clear
212
+ # through normal coord commands" — so name the escape hatch. A live
213
+ # claim means a real Gate A worker is already running (leave it); a
214
+ # dead one is cleared by `coord diagnose <repo> <issue>`.
215
+ raise RuntimeError(
216
+ f"Gate A already in flight: {claim_message(claim)} — if that "
217
+ f"session is dead, clear it with `coord diagnose {repo_name} "
218
+ f"{tracking_issue_number}`, then dispatch again"
219
+ )
220
+
221
+ # Pick the machine.
222
+ if machine_override:
223
+ machine = next(
224
+ (m for m in config.machines if m.name == machine_override),
225
+ None,
226
+ )
227
+ if machine is None:
228
+ raise RuntimeError(f"machine {machine_override!r} not in coordinator.yml")
229
+ if not machine.can_work_on(repo_name):
230
+ raise RuntimeError(
231
+ f"machine {machine_override!r} does not list repo {repo_name!r}"
232
+ )
233
+ else:
234
+ picked: Machine | None = pick_machine(repo_name, board, config)
235
+ if picked is None:
236
+ raise RuntimeError(
237
+ f"no idle machine claims repo {repo_name!r} — mock-author "
238
+ "needs a machine that has the repo cloned"
239
+ )
240
+ machine = picked
241
+
242
+ tracking_title = issue_data.get("title") or f"Milestone #{tracking_issue_number}"
243
+ resolved_model = config.models.default
244
+
245
+ if amend_briefing is not None:
246
+ # #1315: targeted amend — skip the full open-issues fetch, the
247
+ # fresh-render briefing, and reusing the (likely already-merged and
248
+ # deleted) original gate-a branch name.
249
+ briefing = build_mock_author_amend_briefing(
250
+ repo_slug=repo_cfg.github,
251
+ milestone_title=milestone_title,
252
+ milestone_number=milestone_number,
253
+ tracking_issue_number=tracking_issue_number,
254
+ amend_text=amend_briefing,
255
+ )
256
+ proposal = Proposal(
257
+ id=0,
258
+ machine_name=machine.name,
259
+ repo_name=repo_name,
260
+ issue_number=tracking_issue_number,
261
+ issue_title=f"[gate-a-amend] {tracking_title} — contract correction",
262
+ rationale="Gate A contract amendment (coord acceptance mock --amend, #1315)",
263
+ briefing=briefing,
264
+ model=resolved_model,
265
+ type="mock-author",
266
+ required_gates=list(config.pipeline.default_gates),
267
+ )
268
+ else:
269
+ issues = _fetch_milestone_issues(repo_cfg.github, milestone_number)
270
+
271
+ briefing = build_mock_author_briefing(
272
+ repo_slug=repo_cfg.github,
273
+ milestone_title=milestone_title,
274
+ milestone_number=milestone_number,
275
+ tracking_issue_number=tracking_issue_number,
276
+ tracking_issue_body=issue_data.get("body") or "",
277
+ issues=issues,
278
+ driver_kind=driver_cfg.kind,
279
+ driver_mock_glob=driver_cfg.mock,
280
+ )
281
+
282
+ proposal = Proposal(
283
+ id=0,
284
+ machine_name=machine.name,
285
+ repo_name=repo_name,
286
+ issue_number=tracking_issue_number,
287
+ issue_title=f"[gate-a] {tracking_title} — mock + contract",
288
+ rationale="Gate A mock-author dispatch (coord acceptance mock, #930)",
289
+ briefing=briefing,
290
+ model=resolved_model,
291
+ type="mock-author",
292
+ required_gates=list(config.pipeline.default_gates),
293
+ target_branch=f"ms-{milestone_number}-gate-a",
294
+ )
295
+
296
+ from coord.dispatch import dispatch_with_retry, post_briefing # noqa: PLC0415
297
+ from coord.state import record_dispatched # noqa: PLC0415
298
+
299
+ # #1059 review: dispatch_with_retry can raise ValueError (bad machine/repo
300
+ # config) or httpx.HTTPError (agent unreachable/rejected the payload) —
301
+ # neither was previously caught here, so it propagated past this
302
+ # function's documented "raises RuntimeError" contract as a raw
303
+ # traceback. `acceptance_mock_cmd` only catches RuntimeError, so an
304
+ # uncaught one would dump a multi-line Python traceback to stderr instead
305
+ # of a clean `error: ...` line — exactly the "inconsistent/unreadable
306
+ # error" the operator hit, just via a different trigger than the one
307
+ # reproduced. Translate to RuntimeError so every failure path here is a
308
+ # single clean line, no partial dispatch is recorded (record_dispatched
309
+ # below never runs when this raises), and no claim is left dangling.
310
+ try:
311
+ response = dispatch_with_retry(
312
+ proposal,
313
+ config,
314
+ max_retries=config.concurrency.max_retries,
315
+ backoff_base=config.concurrency.backoff_base,
316
+ )
317
+ except (ValueError, httpx.HTTPError) as e:
318
+ raise RuntimeError(f"could not dispatch mock-author to {machine.name!r}: {e}") from e
319
+
320
+ assignment_id = response.get("id") or uuid.uuid4().hex[:12]
321
+
322
+ record_dispatched(
323
+ assignment_id=assignment_id,
324
+ proposal=proposal,
325
+ repo_github=repo_cfg.github,
326
+ provider_name=response.get("_provider_name"),
327
+ )
328
+
329
+ try:
330
+ post_briefing(proposal, config, assignment_id=assignment_id)
331
+ except Exception: # noqa: BLE001 — best-effort, mirrors dispatch_entry
332
+ pass
333
+
334
+ return assignment_id, machine.name