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,218 @@
1
+ """Provider abstraction layer: base ABC and Capabilities descriptor.
2
+
3
+ Every worker-command backend (claude -p, hypothetical alternatives) implements
4
+ :class:`Provider`. Downstream code calls ``provider.build_command(spec)`` and
5
+ ``provider.initial_input(spec)`` rather than importing concrete helpers from
6
+ ``coord.agent`` directly.
7
+
8
+ Re-exports :class:`~coord.worker_events.WorkerSummary` for convenience so
9
+ callers can do ``from coord.providers.base import WorkerSummary`` without
10
+ knowing which module originates it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ # Re-export WorkerSummary so callers can import it from here.
21
+ from coord.worker_events import WorkerSummary # noqa: F401
22
+
23
+ if TYPE_CHECKING:
24
+ from coord.agent import AssignmentSpec
25
+
26
+ __all__ = ["Capabilities", "Provider", "WorkerSummary"]
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Capabilities:
31
+ """What a provider can actually do.
32
+
33
+ Downstream code gates features on these flags instead of silently
34
+ degrading when a provider doesn't support them.
35
+
36
+ Attributes:
37
+ resume: Supports session resume (``--resume``) → gates chat-continue
38
+ (#315).
39
+ inject: Supports mid-session stdin message injection → gates
40
+ ``inject_message``.
41
+ cost_reporting: Emits per-run cost in the log → TUI shows a dollar
42
+ figure rather than "n/a".
43
+ true_system_prompt: Honours a real ``--system-prompt`` flag rather
44
+ than prepending the prompt text to the first user message.
45
+ enforces_deny_list: SAFETY — honours the worker deny-list and tool
46
+ restrictions. Providers that ignore ``--allowedTools`` / deny
47
+ prompts must set this to ``False`` so the coordinator can warn.
48
+ billing_mode: How runs of this provider are billed. One of
49
+ ``"subscription"`` (covered by a fixed-price plan, e.g.
50
+ interactive Claude Code on Max/Pro), ``"metered"`` (billed
51
+ per-token at API rates, e.g. ``claude -p`` after the
52
+ 2026-06-15 metering change), ``"byo_key"`` (uses the
53
+ operator's own API key — cost is theirs), or ``"unknown"``
54
+ for backends whose billing the coordinator can't infer. This
55
+ is the Track-3 routing signal for the June-15 metering
56
+ mitigation (#322): the coordinator and TUI prefer a
57
+ non-metered backend when one is available.
58
+ human_attended_only: STRUCTURAL TOS-COMPLIANCE GATE (#437). When
59
+ ``True``, this provider may NEVER be selected for unattended
60
+ dispatch — the coordinator refuses to route ``coord plan`` /
61
+ ``coord approve`` / auto-review / auto-reassign /
62
+ reconciliation through it. Used for subscription-billed
63
+ interactive backends (e.g. :class:`ClaudePtyProvider`) where
64
+ running unsupervised would violate Anthropic ToS §3.7
65
+ (no headless / agentic use of subscription Claude Code).
66
+ The default is ``False`` — a new provider is automatable
67
+ unless it explicitly opts out (fail-safe: a metered backend
68
+ is safe to gate behind nothing; an interactive subscription
69
+ backend must opt out). Workers run via this provider must
70
+ be launched into a human-attended terminal by ``coord assign
71
+ --interactive`` and HUMAN-CLOSED; no coordinator-side
72
+ completion-sentinel watching, no auto-termination on output,
73
+ no parsing of the TTY to advance pipeline state.
74
+ """
75
+
76
+ resume: bool
77
+ inject: bool
78
+ cost_reporting: bool
79
+ true_system_prompt: bool
80
+ enforces_deny_list: bool
81
+ billing_mode: str
82
+ human_attended_only: bool = False
83
+
84
+
85
+ class Provider(ABC):
86
+ """Abstract base class for worker-command providers.
87
+
88
+ A provider knows how to:
89
+ * Build the argv for spawning the worker subprocess (``build_command``).
90
+ * Produce the initial stdin payload (``initial_input``).
91
+ * Report what capabilities it actually has (``capabilities``).
92
+ * Identify a successful run in the log (``result_marker``).
93
+ * Declare extra environment variables (``env``).
94
+ * Parse a completed log file (``parse_log``).
95
+
96
+ The concrete ``supports_inject()`` method is derived from
97
+ ``capabilities().inject`` to keep them in sync — subclasses should
98
+ *not* override it.
99
+ """
100
+
101
+ @abstractmethod
102
+ def build_command(
103
+ self,
104
+ spec: "AssignmentSpec",
105
+ *,
106
+ resolved_model: str | None = None,
107
+ system_prompt: str | None = None,
108
+ allowed_tools: str | None = None,
109
+ permission_mode: str = "acceptEdits",
110
+ ) -> list[str]:
111
+ """Build the worker subprocess argv for *spec*.
112
+
113
+ Args:
114
+ spec: The assignment spec being dispatched.
115
+ resolved_model: The resolved model identifier to pass (e.g.
116
+ ``"claude-sonnet-4-6"`` after alias expansion). ``None``
117
+ falls back to provider-internal defaults (``spec.model`` for
118
+ :class:`ClaudeProvider`).
119
+ system_prompt: Override the system prompt. ``None`` means the
120
+ provider computes one from ``spec.type``.
121
+ allowed_tools: Override the ``--allowedTools`` value. ``None``
122
+ means the provider computes one from ``spec.type``.
123
+ permission_mode: Override the ``--permission-mode`` value.
124
+ Defaults to ``"acceptEdits"``.
125
+ """
126
+ ...
127
+
128
+ @abstractmethod
129
+ def initial_input(self, spec: "AssignmentSpec") -> bytes:
130
+ """Return the first stdin payload written to the worker process.
131
+
132
+ For stream-json workers this is the JSON-encoded user message
133
+ containing the briefing text.
134
+ """
135
+ ...
136
+
137
+ def supports_inject(self) -> bool:
138
+ """Whether this provider supports mid-session message injection.
139
+
140
+ Derived from ``capabilities().inject`` — never let the two disagree.
141
+ Override ``capabilities()`` to change this; do not override
142
+ ``supports_inject()`` directly.
143
+ """
144
+ return self.capabilities().inject
145
+
146
+ @abstractmethod
147
+ def result_marker(self) -> str:
148
+ """Return a string whose presence in the log signals job completion.
149
+
150
+ The coordinator reads the log in binary mode so callers are
151
+ responsible for encoding this value when comparing against raw bytes.
152
+ """
153
+ ...
154
+
155
+ @abstractmethod
156
+ def env(self) -> dict[str, str]:
157
+ """Extra environment variables to set for the worker subprocess.
158
+
159
+ Merged on top of the base environment in the spawn path. Return an
160
+ empty dict when no extra variables are needed.
161
+ """
162
+ ...
163
+
164
+ @abstractmethod
165
+ def parse_log(
166
+ self, log_path: str | Path, tail_bytes: int = 65536
167
+ ) -> WorkerSummary:
168
+ """Parse a worker log file and return a rolling summary.
169
+
170
+ Args:
171
+ log_path: Path to the worker's log file.
172
+ tail_bytes: When > 0, only the last *tail_bytes* of the file is
173
+ read (cheap live-polling). Pass ``0`` for a full parse.
174
+ """
175
+ ...
176
+
177
+ @abstractmethod
178
+ def capabilities(self) -> Capabilities:
179
+ """Return a descriptor of what this provider can actually do."""
180
+ ...
181
+
182
+ @abstractmethod
183
+ def oneshot_command(
184
+ self,
185
+ *,
186
+ system_prompt: str,
187
+ output_format: str | None = "json",
188
+ ) -> list[str]:
189
+ """Return the argv for a non-looping, one-shot prompt call.
190
+
191
+ This is distinct from :meth:`build_command`, which produces the
192
+ streaming worker command (``--input-format stream-json``, etc.).
193
+ One-shot calls are used by coordinator-side code — brain planning
194
+ and the dashboard assistant — that needs a single prompt/response
195
+ round, not a full streaming worker session.
196
+
197
+ The caller is responsible for piping the user message via *stdin*
198
+ (for :class:`~.claude.ClaudeProvider`). Providers where stdin
199
+ delivery is not applicable (e.g.
200
+ :class:`~.opencode.OpenCodeProvider`) silently ignore the user
201
+ message and the *output_format* kwarg; callers should document
202
+ that the one-shot path is best-effort for those backends.
203
+
204
+ Args:
205
+ system_prompt: The system prompt for the call.
206
+ output_format: Requested output format. ``"json"`` asks for a
207
+ structured JSON response (as the brain expects);
208
+ ``None`` omits any output-format flag so the backend
209
+ streams plain text (used by the dashboard assistant).
210
+ Providers that do not support the requested format
211
+ silently ignore this kwarg.
212
+
213
+ Returns:
214
+ A list of strings suitable for passing to
215
+ :func:`subprocess.run` or
216
+ :func:`asyncio.create_subprocess_exec`.
217
+ """
218
+ ...
@@ -0,0 +1,284 @@
1
+ """ClaudeProvider: the ``claude -p`` concrete provider.
2
+
3
+ Parity requirement: ``ClaudeProvider().build_command(spec)`` produces the
4
+ **same argv** as ``coord.agent.default_worker_command(spec)`` for the same
5
+ inputs. The logic is a direct transcription of that function's body with
6
+ the ``resolved_model`` / ``system_prompt`` / ``allowed_tools`` /
7
+ ``permission_mode`` kwargs spliced in; the parity tests in
8
+ ``tests/test_providers.py`` enforce this mechanically.
9
+
10
+ Imports from ``coord.agent`` are **deferred** (inside method bodies) to keep
11
+ the import cycle latent until the wiring issue lands. At that point
12
+ ``AgentServer`` will consume ``ClaudeProvider``, creating a two-way link;
13
+ until then the one-way ``claude → agent`` direction is safe because
14
+ ``coord.agent`` does not import from ``coord.providers``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ from coord.providers.base import Capabilities, Provider, WorkerSummary
23
+
24
+ if TYPE_CHECKING:
25
+ from coord.agent import AssignmentSpec
26
+
27
+
28
+ class ClaudeProvider(Provider):
29
+ """Concrete provider for ``claude -p`` (Anthropic Claude Code workers).
30
+
31
+ This is the **reference backend** — all :class:`~.base.Capabilities`
32
+ flags are ``True`` because ``claude -p`` supports every feature the
33
+ coordinator relies on.
34
+
35
+ Args:
36
+ binary: Override the worker binary name/path. ``None`` falls back to
37
+ :data:`coord.agent.DEFAULT_WORKER_BINARY` (``"claude"``).
38
+ model: Fallback model id/alias from the provider definition
39
+ (``ProviderDef.model``). Used only when neither an explicit
40
+ ``resolved_model`` nor ``spec.model`` is set — see
41
+ :meth:`build_command`.
42
+ env: Extra environment variables from the provider definition
43
+ (``ProviderDef.env``, already ``${VAR}``-expanded by config
44
+ parsing). Returned verbatim by :meth:`env`.
45
+ extra_args: Additional argv entries from the provider definition
46
+ (``ProviderDef.extra_args``). Appended to the end of the argv
47
+ built by :meth:`build_command`.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ binary: str | None = None,
53
+ *,
54
+ model: str | None = None,
55
+ env: dict[str, str] | None = None,
56
+ extra_args: list[str] | None = None,
57
+ ) -> None:
58
+ self._binary = binary
59
+ self._model = model
60
+ self._env = dict(env) if env else {}
61
+ self._extra_args = list(extra_args) if extra_args else []
62
+
63
+ # ── Capabilities ──────────────────────────────────────────────────────────
64
+
65
+ def capabilities(self) -> Capabilities:
66
+ """All capabilities enabled — claude -p is the reference backend.
67
+
68
+ ``billing_mode="metered"`` reflects the 2026-06-15 Anthropic
69
+ change: ``claude -p`` (and Agent SDK) sessions are billed at full
70
+ API rates against a small non-rolling credit pool, not against
71
+ the Max/Pro subscription. Downstream code uses this flag to
72
+ prefer a non-metered backend when one is available (#322).
73
+ """
74
+ return Capabilities(
75
+ resume=True,
76
+ inject=True,
77
+ cost_reporting=True,
78
+ true_system_prompt=True,
79
+ enforces_deny_list=True,
80
+ billing_mode="metered",
81
+ # #437: claude -p is an unattended/headless backend — billed
82
+ # per-token and intended for autonomous dispatch. This is the
83
+ # COMPLIANT automatable path and must NOT be flagged as
84
+ # human-attended. Set explicitly (not by default) to make the
85
+ # intent visible in code.
86
+ human_attended_only=False,
87
+ )
88
+
89
+ # ── Core methods ──────────────────────────────────────────────────────────
90
+
91
+ def build_command( # noqa: PLR0912 (many spec.type branches, matches legacy)
92
+ self,
93
+ spec: "AssignmentSpec",
94
+ *,
95
+ resolved_model: str | None = None,
96
+ system_prompt: str | None = None,
97
+ allowed_tools: str | None = None,
98
+ permission_mode: str = "acceptEdits",
99
+ ) -> list[str]:
100
+ """Build the ``claude -p`` argv for *spec*.
101
+
102
+ Produces the **same argv** as ``default_worker_command(spec)`` when
103
+ called with ``resolved_model=spec.model`` and without overriding
104
+ ``system_prompt`` / ``allowed_tools`` / ``permission_mode``.
105
+ """
106
+ # Deferred import — keeps the cycle latent until wiring is done.
107
+ from coord.agent import ( # noqa: PLC0415
108
+ DEFAULT_WORKER_BINARY,
109
+ MILESTONE_CHAT_DENY_COMMANDS,
110
+ MILESTONE_CHAT_SYSTEM_PROMPT,
111
+ MOCK_AUTHOR_DENY_COMMANDS,
112
+ MOCK_AUTHOR_SYSTEM_PROMPT,
113
+ NEW_ISSUE_CHAT_DENY_COMMANDS,
114
+ NEW_ISSUE_CHAT_SYSTEM_PROMPT,
115
+ REFINEMENT_SYSTEM_PROMPT,
116
+ TEST_CHAT_SYSTEM_PROMPT,
117
+ WORKER_PLAN_PROMPT,
118
+ WORKER_SYSTEM_PROMPT,
119
+ _base_checkout_write_guard_tools,
120
+ _sealed_write_guard_tools,
121
+ build_deny_prompt,
122
+ )
123
+
124
+ binary = self._binary if self._binary is not None else DEFAULT_WORKER_BINARY
125
+
126
+ # Precedence: explicit resolved_model > spec.model > provider-
127
+ # definition model (ProviderDef.model, threaded in via __init__).
128
+ # A plain ClaudeProvider().build_command(spec) with no definition
129
+ # model still matches default_worker_command (self._model is None).
130
+ if resolved_model is not None:
131
+ effective_model = resolved_model
132
+ elif spec.model is not None:
133
+ effective_model = spec.model
134
+ else:
135
+ effective_model = self._model
136
+
137
+ # Compute system_prompt / allowed_tools from spec.type when not
138
+ # provided — direct transcription of default_worker_command's logic.
139
+ if system_prompt is None or allowed_tools is None:
140
+ if spec.type == "plan":
141
+ _sp = spec.system_prompt if spec.system_prompt else WORKER_PLAN_PROMPT
142
+ _at = "Read,Bash"
143
+ elif spec.type == "refinement":
144
+ _sp = spec.system_prompt if spec.system_prompt else REFINEMENT_SYSTEM_PROMPT
145
+ _at = "Read"
146
+ elif spec.type == "test-chat":
147
+ _sp = spec.system_prompt if spec.system_prompt else TEST_CHAT_SYSTEM_PROMPT
148
+ _sp += build_deny_prompt(spec.deny_commands)
149
+ _at = "Read,Bash"
150
+ elif spec.type == "new-issue-chat":
151
+ _sp = spec.system_prompt if spec.system_prompt else NEW_ISSUE_CHAT_SYSTEM_PROMPT
152
+ _sp += build_deny_prompt(NEW_ISSUE_CHAT_DENY_COMMANDS)
153
+ if spec.new_issue_guidance:
154
+ _sp += (
155
+ "\n\nThe user's repo has the following guidance for "
156
+ "new-issue drafts. Follow it: ask focused questions "
157
+ "matched to the required sections, then produce a "
158
+ "finalised issue body using the same structure. Do not "
159
+ "invent sections that aren't there; do not omit required "
160
+ "sections (mark them `(TBD)` if the conversation hasn't "
161
+ "covered them yet).\n\n"
162
+ + spec.new_issue_guidance
163
+ )
164
+ _at = "Read,Bash"
165
+ elif spec.type == "milestone-chat":
166
+ _sp = spec.system_prompt if spec.system_prompt else MILESTONE_CHAT_SYSTEM_PROMPT
167
+ _sp += build_deny_prompt(MILESTONE_CHAT_DENY_COMMANDS)
168
+ _at = "Read,Bash"
169
+ elif spec.type == "mock-author":
170
+ _sp = spec.system_prompt if spec.system_prompt else MOCK_AUTHOR_SYSTEM_PROMPT
171
+ _sp += build_deny_prompt(MOCK_AUTHOR_DENY_COMMANDS)
172
+ _at = "Read,Edit,Write,Bash"
173
+ else:
174
+ _sp = spec.system_prompt if spec.system_prompt else WORKER_SYSTEM_PROMPT
175
+ _sp += build_deny_prompt(spec.deny_commands)
176
+ _at = "Read,Edit,Write,Bash"
177
+
178
+ if system_prompt is None:
179
+ system_prompt = _sp
180
+ if allowed_tools is None:
181
+ allowed_tools = _at
182
+
183
+ argv: list[str] = [
184
+ binary, "-p",
185
+ "--input-format", "stream-json",
186
+ "--output-format", "stream-json",
187
+ "--verbose",
188
+ "--system-prompt", system_prompt,
189
+ "--allowedTools", allowed_tools,
190
+ "--permission-mode", permission_mode,
191
+ # #1445: see the matching comment in default_worker_command —
192
+ # workers must not inherit the host checkout's project/local
193
+ # Claude Code settings.
194
+ "--setting-sources", "user",
195
+ ]
196
+ if effective_model:
197
+ argv.extend(["--model", effective_model])
198
+ # #1315 / #1642: same structural write guards as
199
+ # default_worker_command — sealed-oracle prefix plus (for any type
200
+ # with Edit in --allowedTools) the shared base checkout.
201
+ disallowed_tools = _sealed_write_guard_tools(spec.files_forbidden)
202
+ if "Edit" in allowed_tools:
203
+ for pattern in _base_checkout_write_guard_tools(spec.repo_path):
204
+ if pattern not in disallowed_tools:
205
+ disallowed_tools.append(pattern)
206
+ if disallowed_tools:
207
+ argv.extend(["--disallowedTools", ",".join(disallowed_tools)])
208
+ if spec.resume_session_id:
209
+ argv.extend(["--resume", spec.resume_session_id])
210
+ # #1706: provider-definition extra_args go last, after every flag
211
+ # this method itself constructs.
212
+ if self._extra_args:
213
+ argv.extend(self._extra_args)
214
+ return argv
215
+
216
+ def oneshot_command(
217
+ self,
218
+ *,
219
+ system_prompt: str,
220
+ output_format: str | None = "json",
221
+ ) -> list[str]:
222
+ """Build the argv for a one-shot ``claude -p`` call.
223
+
224
+ Returns ``[binary, "-p", "--system-prompt", system_prompt]`` plus
225
+ ``["--output-format", output_format]`` when *output_format* is
226
+ non-``None``.
227
+
228
+ Unlike :meth:`build_command`, this does **not** add
229
+ ``--input-format stream-json``, ``--verbose``,
230
+ ``--allowedTools``, or ``--permission-mode`` — those flags are
231
+ only meaningful for streaming worker sessions.
232
+
233
+ The user message is expected to arrive via *stdin* (the caller
234
+ passes ``input=`` to :func:`subprocess.run` or writes to
235
+ ``proc.stdin`` in the async path).
236
+
237
+ Note: unlike :meth:`build_command`, this does **not** thread in
238
+ ``self._model`` / ``self._extra_args`` / ``self._env`` (#1706) —
239
+ oneshot calls (brain planning, dashboard assistant) don't take an
240
+ ``AssignmentSpec`` and sit outside the assignment spawn path that
241
+ #1706 wires provider-definition config into. A future reader
242
+ should not assume oneshot calls honour ``providers.definitions.
243
+ <name>.{model,extra_args,env}``.
244
+
245
+ Args:
246
+ system_prompt: The system prompt for the one-shot call.
247
+ output_format: Output format flag value. ``"json"`` produces
248
+ ``--output-format json`` (brain planning expects this so
249
+ it can extract the ``result`` field from the outer JSON
250
+ object). ``None`` omits the flag (dashboard assistant
251
+ streams the raw text response line-by-line).
252
+ """
253
+ from coord.agent import DEFAULT_WORKER_BINARY # noqa: PLC0415
254
+ binary = self._binary if self._binary is not None else DEFAULT_WORKER_BINARY
255
+ cmd = [binary, "-p", "--system-prompt", system_prompt]
256
+ if output_format is not None:
257
+ cmd.extend(["--output-format", output_format])
258
+ return cmd
259
+
260
+ def initial_input(self, spec: "AssignmentSpec") -> bytes:
261
+ """Return the briefing encoded as a stream-json user message."""
262
+ from coord.agent import _user_message_line # noqa: PLC0415
263
+ return _user_message_line(spec.briefing)
264
+
265
+ def result_marker(self) -> str:
266
+ """String whose presence in the log signals logical completion."""
267
+ return '"type":"result"'
268
+
269
+ def env(self) -> dict[str, str]:
270
+ """Extra environment variables from the provider definition (#1706).
271
+
272
+ Returns a copy of ``ProviderDef.env`` (already ``${VAR}``-expanded
273
+ by config parsing) threaded in via ``__init__``. Empty dict when
274
+ the provider was constructed with no ``env`` (matches pre-#1706
275
+ behaviour for no-config deployments).
276
+ """
277
+ return dict(self._env)
278
+
279
+ def parse_log(
280
+ self, log_path: str | Path, tail_bytes: int = 65536
281
+ ) -> WorkerSummary:
282
+ """Delegate to :func:`coord.worker_events.parse_log`."""
283
+ from coord.worker_events import parse_log # noqa: PLC0415
284
+ return parse_log(log_path, tail_bytes=tail_bytes)