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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Shared infra for coord/commands/*.py: config loading, the shared
|
|
2
|
+
``--config`` option, port constants, and the handful of helpers used by
|
|
3
|
+
more than one command module. Extracted from coord/cli.py (#747).
|
|
4
|
+
|
|
5
|
+
#1237 (PKG-1): this module is imported by *every* command module, including
|
|
6
|
+
the client-clean ones, so it is the one file that absolutely must not reach
|
|
7
|
+
the server stack at import time. Keep it that way:
|
|
8
|
+
|
|
9
|
+
- No module-scope import of ``coord.serve_app`` / ``coord.agent_app`` /
|
|
10
|
+
``coord.dashboard.*`` / ``starlette`` / ``uvicorn`` / ``psutil``. The port
|
|
11
|
+
constants below are deliberately *duplicated literals* rather than
|
|
12
|
+
re-exports for this reason — importing ``coord.serve_app`` just to read
|
|
13
|
+
``SERVE_PORT`` would drag starlette into ``coord status``.
|
|
14
|
+
- Server-only work goes behind :func:`server_extra_guard`, which turns the
|
|
15
|
+
resulting ``ModuleNotFoundError`` into an actionable "install the [server]
|
|
16
|
+
extra" message.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import socket
|
|
23
|
+
import sys
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from coord.config import Config, ConfigError, load, resolve_config_path
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
33
|
+
from collections.abc import Iterator
|
|
34
|
+
|
|
35
|
+
# Canonical constant in coord.brain.AGENT_PORT — duplicated here as a literal
|
|
36
|
+
# so the CLI decorator default doesn't have to import the brain (and, through
|
|
37
|
+
# it, the provider stack) on every `coord` invocation.
|
|
38
|
+
AGENT_PORT = 7433
|
|
39
|
+
# Portable control-center daemon port (#584); canonical constant in
|
|
40
|
+
# coord.serve_app.SERVE_PORT — duplicated here for the CLI decorator default,
|
|
41
|
+
# mirroring the AGENT_PORT pattern above.
|
|
42
|
+
SERVE_PORT = 7435
|
|
43
|
+
|
|
44
|
+
#: Distributions provided by the ``[server]`` extra (#1237). A
|
|
45
|
+
#: ``ModuleNotFoundError`` naming any of these from a server codepath means
|
|
46
|
+
#: "base install, no extra" — not a bug.
|
|
47
|
+
SERVER_EXTRA_MODULES = frozenset({"starlette", "uvicorn", "websockets", "psutil"})
|
|
48
|
+
|
|
49
|
+
#: What to tell a user who hit a server command on a client-only install.
|
|
50
|
+
SERVER_EXTRA_INSTALL_HINT = "pip install 'code-coordinator[server]'"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def server_extra_guard(feature: str) -> "Iterator[None]":
|
|
55
|
+
"""Translate a missing ``[server]`` extra into an actionable CLI error.
|
|
56
|
+
|
|
57
|
+
``pip install code-coordinator`` installs a *client* (#1237): no
|
|
58
|
+
starlette/uvicorn/websockets/psutil. Wrap the function-local imports that
|
|
59
|
+
boot a server in this so the failure reads as "you need the extra" rather
|
|
60
|
+
than a raw ``ModuleNotFoundError: No module named 'uvicorn'`` traceback::
|
|
61
|
+
|
|
62
|
+
with server_extra_guard("serve"):
|
|
63
|
+
import uvicorn
|
|
64
|
+
from coord.serve_app import build_app
|
|
65
|
+
|
|
66
|
+
*feature* is the user-facing command name (``"serve"``, ``"web"``,
|
|
67
|
+
``"agent"``) and is echoed back in the message.
|
|
68
|
+
|
|
69
|
+
Anything that is *not* one of the extra's modules re-raises untouched — a
|
|
70
|
+
genuinely broken import inside ``coord.serve_app`` must not be papered
|
|
71
|
+
over as a packaging problem.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
except ModuleNotFoundError as exc:
|
|
76
|
+
missing = (exc.name or "").split(".")[0]
|
|
77
|
+
if missing not in SERVER_EXTRA_MODULES:
|
|
78
|
+
raise
|
|
79
|
+
raise click.ClickException(
|
|
80
|
+
f"`coord {feature}` needs the server extra, which is not installed "
|
|
81
|
+
f"(missing {missing!r}).\n"
|
|
82
|
+
f" Install it with: {SERVER_EXTRA_INSTALL_HINT}\n"
|
|
83
|
+
" The base `code-coordinator` install is a client-only CLI — it "
|
|
84
|
+
"can drive a remote fleet, but not host one (#1237)."
|
|
85
|
+
) from exc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_CONFIG_OPTION = click.option(
|
|
89
|
+
"--config",
|
|
90
|
+
"config_path",
|
|
91
|
+
type=click.Path(path_type=Path),
|
|
92
|
+
# Callable default: resolved per-invocation to a concrete Path so every
|
|
93
|
+
# command receives a real path (never None). Resolution order:
|
|
94
|
+
# $COORD_CONFIG → ~/.coord/coordinator.yml → ./coordinator.yml.
|
|
95
|
+
default=resolve_config_path,
|
|
96
|
+
help=(
|
|
97
|
+
"Path to coordinator.yml. Default resolution: $COORD_CONFIG, then "
|
|
98
|
+
"~/.coord/coordinator.yml, then ./coordinator.yml."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _save_config_snapshot(config: Config) -> None:
|
|
104
|
+
"""Persist machine + pipeline metadata to the DB so dashboards can read it.
|
|
105
|
+
|
|
106
|
+
Writes:
|
|
107
|
+
- ``machines`` rows (used by the web dashboard + the TUI Machines view)
|
|
108
|
+
- ``board_meta['pipeline_default_gates']`` JSON list of default gates
|
|
109
|
+
- ``board_meta['pipeline_tracked_labels']`` JSON list of tracked GitHub
|
|
110
|
+
issue labels (defaults to ``['coord']`` when unconfigured)
|
|
111
|
+
|
|
112
|
+
The pipeline keys let the TUI Pipeline panel pick up coordinator.yml
|
|
113
|
+
settings without having to parse YAML itself.
|
|
114
|
+
"""
|
|
115
|
+
# #584: a thin client (board_service configured) must not create/write a
|
|
116
|
+
# local DB — the daemon/host owns the config snapshot. On the host
|
|
117
|
+
# board_service is unset, so the snapshot is written as before.
|
|
118
|
+
from coord.client import resolve_board_service
|
|
119
|
+
if resolve_board_service() is not None:
|
|
120
|
+
return
|
|
121
|
+
conn = None
|
|
122
|
+
try:
|
|
123
|
+
from coord.db import get_connection
|
|
124
|
+
conn = get_connection()
|
|
125
|
+
conn.execute("DELETE FROM machines")
|
|
126
|
+
for m in config.machines:
|
|
127
|
+
conn.execute(
|
|
128
|
+
"INSERT INTO machines (name, host, capabilities, repos) VALUES (?, ?, ?, ?)",
|
|
129
|
+
(m.name, m.host, json.dumps(m.capabilities), json.dumps(m.repos)),
|
|
130
|
+
)
|
|
131
|
+
conn.execute(
|
|
132
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
133
|
+
"('pipeline_default_gates', ?)",
|
|
134
|
+
(json.dumps(list(config.pipeline.default_gates)),),
|
|
135
|
+
)
|
|
136
|
+
conn.execute(
|
|
137
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
138
|
+
"('pipeline_tracked_labels', ?)",
|
|
139
|
+
(json.dumps(config.pipeline.tracked_labels()),),
|
|
140
|
+
)
|
|
141
|
+
# Repo name → GitHub slug map: the TUI pipeline panel uses this to
|
|
142
|
+
# translate a `gh search issues` repository.nameWithOwner back into
|
|
143
|
+
# the coord-local repo name expected by `coord assign`.
|
|
144
|
+
conn.execute(
|
|
145
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
146
|
+
"('pipeline_repos', ?)",
|
|
147
|
+
(json.dumps({r.name: r.github for r in config.repos}),),
|
|
148
|
+
)
|
|
149
|
+
# #296: run_cmd per repo — TUI surfaces this in the Test stage
|
|
150
|
+
# detail panel as the "Run" row so the tester knows what to launch.
|
|
151
|
+
# Only repos that have a run_cmd are included; absent → no entry.
|
|
152
|
+
conn.execute(
|
|
153
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
154
|
+
"('pipeline_repo_run_cmds', ?)",
|
|
155
|
+
(json.dumps({r.name: r.run_cmd for r in config.repos if r.run_cmd is not None}),),
|
|
156
|
+
)
|
|
157
|
+
# Whether the pipeline includes a Plan gate before Work. Sourced
|
|
158
|
+
# from dispatch.require_plan — when true, the TUI prepends a Plan
|
|
159
|
+
# stage and Work [Go] becomes "approve plan" rather than fresh
|
|
160
|
+
# dispatch.
|
|
161
|
+
conn.execute(
|
|
162
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
163
|
+
"('pipeline_require_plan', ?)",
|
|
164
|
+
("1" if config.dispatch.require_plan else "0",),
|
|
165
|
+
)
|
|
166
|
+
# #803: models config snapshot — TUI reads this to show which model
|
|
167
|
+
# tier will be used for an interactive --fix-of without needing to
|
|
168
|
+
# parse coordinator.yml itself.
|
|
169
|
+
conn.execute(
|
|
170
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
171
|
+
"('pipeline_models', ?)",
|
|
172
|
+
(json.dumps({
|
|
173
|
+
"default": config.models.default,
|
|
174
|
+
"escalation": config.models.escalation,
|
|
175
|
+
"escalate_fix_model": config.pipeline.escalate_fix_model,
|
|
176
|
+
}),),
|
|
177
|
+
)
|
|
178
|
+
# #349: repo_name → local-checkout path for the machine running this
|
|
179
|
+
# coordinator. Used by the TUI to read git branch HEADs when
|
|
180
|
+
# detecting test-plan staleness. Only includes repos that have a
|
|
181
|
+
# repo_paths entry on the matching machine (hostname-matched first;
|
|
182
|
+
# any machine as fallback).
|
|
183
|
+
local_hostname = socket.gethostname().split(".")[0]
|
|
184
|
+
repo_paths_map: dict[str, str] = {}
|
|
185
|
+
# Try hostname-matched machine first, then fall back to all machines.
|
|
186
|
+
for pass_no in range(2):
|
|
187
|
+
for m in config.machines:
|
|
188
|
+
on_this_machine = (
|
|
189
|
+
m.name == local_hostname
|
|
190
|
+
or m.host.split(".")[0] == local_hostname
|
|
191
|
+
)
|
|
192
|
+
if pass_no == 0 and not on_this_machine:
|
|
193
|
+
continue
|
|
194
|
+
for rn in m.repos:
|
|
195
|
+
if rn not in repo_paths_map:
|
|
196
|
+
p = m.repo_path(rn)
|
|
197
|
+
if p:
|
|
198
|
+
repo_paths_map[rn] = str(Path(p).expanduser())
|
|
199
|
+
conn.execute(
|
|
200
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
201
|
+
"('pipeline_repo_paths', ?)",
|
|
202
|
+
(json.dumps(repo_paths_map),),
|
|
203
|
+
)
|
|
204
|
+
# #1151: repo_name -> route `match` globs, for repos whose acceptance
|
|
205
|
+
# driver is *routed* (acceptance.drivers.<repo>.routes non-empty,
|
|
206
|
+
# #1125). Unrouted repos (flat driver or no driver at all) are
|
|
207
|
+
# omitted entirely. The TUI's Pipeline right-click acceptance actions
|
|
208
|
+
# (`dispatch_gate_a_mock_for_selected_pipeline_row` /
|
|
209
|
+
# `dispatch_acceptance_author_for_selected_pipeline_row` /
|
|
210
|
+
# `dispatch_acceptance_record_for_selected_pipeline_row`, all in
|
|
211
|
+
# tui/src/app/pipeline.rs) were firing `coord acceptance mock/author/
|
|
212
|
+
# record` with no `--for-path`, which those CLI commands reject with
|
|
213
|
+
# "no route matched" the moment a repo's driver becomes routed. This
|
|
214
|
+
# lets the TUI auto-resolve the unambiguous (single-route) case and
|
|
215
|
+
# surface a clear, actionable warning — instead of a raw CLI error —
|
|
216
|
+
# when more than one route exists and it can't tell which applies.
|
|
217
|
+
acceptance_routes_map: dict[str, list[str]] = {
|
|
218
|
+
repo_name: [route.match for route in driver.routes]
|
|
219
|
+
for repo_name, driver in config.acceptance.drivers.items()
|
|
220
|
+
if driver.routes
|
|
221
|
+
}
|
|
222
|
+
conn.execute(
|
|
223
|
+
"INSERT OR REPLACE INTO board_meta (key, value) VALUES "
|
|
224
|
+
"('pipeline_acceptance_routes', ?)",
|
|
225
|
+
(json.dumps(acceptance_routes_map),),
|
|
226
|
+
)
|
|
227
|
+
conn.commit()
|
|
228
|
+
except Exception: # noqa: BLE001 — non-critical, don't abort CLI
|
|
229
|
+
if conn is not None:
|
|
230
|
+
try:
|
|
231
|
+
conn.rollback()
|
|
232
|
+
except Exception: # noqa: BLE001
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _load_config(path: Path | None) -> Config:
|
|
237
|
+
# Resolve the default location ($COORD_CONFIG → ~/.coord/coordinator.yml →
|
|
238
|
+
# ./coordinator.yml) when no explicit --config was given, so `coord` works on
|
|
239
|
+
# a machine without a repo checkout and isn't sensitive to the CWD.
|
|
240
|
+
if path is None:
|
|
241
|
+
from coord.config import resolve_config_path # noqa: PLC0415
|
|
242
|
+
|
|
243
|
+
path = resolve_config_path()
|
|
244
|
+
# #1080: "am I a thin client" (board_service configured) is the PRIMARY
|
|
245
|
+
# branch, checked before "does a local file exist" — not after. A thin
|
|
246
|
+
# client must never trust a local coordinator.yml, even one that happens
|
|
247
|
+
# to exist: a stray ~/.coord/coordinator.yml or ./coordinator.yml can
|
|
248
|
+
# silently diverge from the daemon's real config with no signal that it's
|
|
249
|
+
# stale (#947 friction log — a 7-week-old symlink shadowed the daemon's
|
|
250
|
+
# config on every command). On a machine with no client.toml/board_service
|
|
251
|
+
# (svc is None — e.g. the daemon host), this is a no-op and local-file
|
|
252
|
+
# resolution proceeds exactly as before (#584/#591).
|
|
253
|
+
try:
|
|
254
|
+
from coord.client import resolve_board_service # noqa: PLC0415
|
|
255
|
+
|
|
256
|
+
svc = resolve_board_service()
|
|
257
|
+
if svc is not None:
|
|
258
|
+
from coord.client import fetch_remote_config # noqa: PLC0415
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
path = fetch_remote_config(svc)
|
|
262
|
+
except Exception as exc: # noqa: BLE001 — do NOT fall through to
|
|
263
|
+
# load(path): path may point at a local file that happens to
|
|
264
|
+
# exist (the exact bypass this issue closes). Fail loudly
|
|
265
|
+
# instead of silently trusting whatever is on disk.
|
|
266
|
+
raise ConfigError(
|
|
267
|
+
f"could not fetch config from {svc.url}: {exc}"
|
|
268
|
+
) from exc
|
|
269
|
+
cfg = load(path)
|
|
270
|
+
except ConfigError as e:
|
|
271
|
+
click.echo(f"error: {e}", err=True)
|
|
272
|
+
sys.exit(2)
|
|
273
|
+
_save_config_snapshot(cfg)
|
|
274
|
+
return cfg
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _not_implemented(name: str) -> None:
|
|
278
|
+
click.echo(f"coord {name}: not implemented yet (stub)", err=True)
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _apply_label_change(
|
|
283
|
+
repo: str,
|
|
284
|
+
issue: int,
|
|
285
|
+
config_path: Path,
|
|
286
|
+
*,
|
|
287
|
+
add: set[str],
|
|
288
|
+
remove_if_present: set[str],
|
|
289
|
+
success_message: str,
|
|
290
|
+
no_op_message: str | None = None,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Shared backbone for the lifecycle label-change commands
|
|
293
|
+
(#260/#261/#266/#802).
|
|
294
|
+
|
|
295
|
+
Resolves *repo* via ``coordinator.yml``, then delegates to
|
|
296
|
+
``state.apply_issue_labels`` which routes through the daemon seam
|
|
297
|
+
(GitHub via ``gh`` today; GitLab / bare-DB later) — the same seam
|
|
298
|
+
``coord issue label`` uses. The local ``issues`` cache is updated
|
|
299
|
+
inside the seam so the TUI reflects the change on its next tick.
|
|
300
|
+
|
|
301
|
+
``no_op_message`` (optional) is echoed when no labels were actually
|
|
302
|
+
added or removed — used by ``coord backlog`` to say "already in
|
|
303
|
+
Backlog" instead of making a no-op ``gh`` call.
|
|
304
|
+
"""
|
|
305
|
+
from coord.state import apply_issue_labels # noqa: PLC0415
|
|
306
|
+
|
|
307
|
+
cfg = _load_config(config_path)
|
|
308
|
+
repo_entry = cfg.repo(repo)
|
|
309
|
+
if repo_entry is None:
|
|
310
|
+
click.echo(f"error: unknown repo {repo!r} (not in coordinator.yml)", err=True)
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
slug = repo_entry.github
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
_new_labels, changed = apply_issue_labels(
|
|
316
|
+
repo, issue,
|
|
317
|
+
add=add,
|
|
318
|
+
remove=remove_if_present,
|
|
319
|
+
repo_github=slug,
|
|
320
|
+
)
|
|
321
|
+
except Exception as e: # noqa: BLE001
|
|
322
|
+
click.echo(f"error: label change failed: {e}", err=True)
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
|
|
325
|
+
if not changed and no_op_message is not None:
|
|
326
|
+
click.echo(no_op_message)
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
click.echo(success_message)
|