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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
coord/providers/base.py
ADDED
|
@@ -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)
|