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/test_chat.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""#314 Phase B: dispatch a `type="test-chat"` session for a completed work assignment.
|
|
2
|
+
|
|
3
|
+
Used by `coord test-chat <work_assignment_id>` (the CLI command shelled out by
|
|
4
|
+
the TUI's T keybind on a Test-stage issue). Builds a seed briefing that
|
|
5
|
+
pre-loads the worker with the PR diff, the most recent build log, the
|
|
6
|
+
worker's SMOKE_TESTS block, the repo's run command, and the repo's CLAUDE.md
|
|
7
|
+
— the context a developer needs to validate the change.
|
|
8
|
+
|
|
9
|
+
The session itself runs as a `type="test-chat"` assignment on an agent
|
|
10
|
+
server. Tools are restricted to ``Read,Bash`` (see ``coord/agent.py``); write-
|
|
11
|
+
side Bash commands are blocked by the deny list injected into the system
|
|
12
|
+
prompt. The developer drives the conversation via ``POST /inject/{id}`` from
|
|
13
|
+
the TUI.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from coord.config import Config
|
|
26
|
+
from coord.dispatch import AGENT_PORT
|
|
27
|
+
from coord.models import Assignment, Machine, Repo
|
|
28
|
+
|
|
29
|
+
# Soft caps so the seed briefing stays bounded. Diffs on large refactors can
|
|
30
|
+
# run to tens of thousands of lines; build logs can be even larger.
|
|
31
|
+
MAX_DIFF_LINES = 500
|
|
32
|
+
MAX_BUILD_LOG_LINES = 200
|
|
33
|
+
MAX_CLAUDE_MD_CHARS = 8000
|
|
34
|
+
|
|
35
|
+
# Commands the test-chat worker is not allowed to run. Injected into the
|
|
36
|
+
# AssignmentSpec payload so ``agent.py`` can surface them in the deny prompt.
|
|
37
|
+
_TEST_CHAT_DENY_COMMANDS = [
|
|
38
|
+
"Bash(gh *)",
|
|
39
|
+
"Bash(git push *)",
|
|
40
|
+
"Bash(git commit *)",
|
|
41
|
+
"Bash(git checkout *)",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pick_test_chat_machine(cfg: Config, repo: str) -> Machine | None:
|
|
46
|
+
"""Pick a machine to run the test-chat session on.
|
|
47
|
+
|
|
48
|
+
Returns the first reachable machine that lists *repo* and is not paused.
|
|
49
|
+
Test-chat is read-plus-bash and short-lived, so any qualified machine
|
|
50
|
+
works — no freshness / capacity weighting. Returns ``None`` when no
|
|
51
|
+
machine claims the repo.
|
|
52
|
+
"""
|
|
53
|
+
from coord.machine_pause import paused_set # noqa: PLC0415
|
|
54
|
+
|
|
55
|
+
paused = paused_set(cfg.machines)
|
|
56
|
+
for m in cfg.machines:
|
|
57
|
+
if (
|
|
58
|
+
m.can_work_on(repo)
|
|
59
|
+
and m.repo_path(repo) is not None
|
|
60
|
+
and m.name not in paused
|
|
61
|
+
):
|
|
62
|
+
return m
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _fetch_diff(branch: str, repo_root: Path) -> str:
|
|
67
|
+
"""Best-effort fetch of the diff between *branch* and its upstream base.
|
|
68
|
+
|
|
69
|
+
Tries ``git diff origin/main...HEAD`` (triple-dot — shows only the commits
|
|
70
|
+
on the feature branch). Falls back to a two-dot diff against main, then
|
|
71
|
+
gives up with a placeholder. Clamped to ``MAX_DIFF_LINES`` lines so the
|
|
72
|
+
seed stays bounded even for large PRs.
|
|
73
|
+
"""
|
|
74
|
+
diff_cmds: list[list[str]] = [
|
|
75
|
+
["git", "diff", "origin/main...", "HEAD"],
|
|
76
|
+
["git", "diff", f"origin/main...{branch}"],
|
|
77
|
+
["git", "diff", "main...", branch],
|
|
78
|
+
]
|
|
79
|
+
for cmd in diff_cmds:
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
cmd,
|
|
83
|
+
cwd=str(repo_root),
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=15,
|
|
87
|
+
)
|
|
88
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
89
|
+
lines = result.stdout.splitlines()
|
|
90
|
+
if len(lines) > MAX_DIFF_LINES:
|
|
91
|
+
truncated = len(lines) - MAX_DIFF_LINES
|
|
92
|
+
lines = lines[:MAX_DIFF_LINES]
|
|
93
|
+
lines.append(f"…[{truncated} more diff lines truncated]")
|
|
94
|
+
return "\n".join(lines)
|
|
95
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
96
|
+
continue
|
|
97
|
+
return "(diff not available)"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_build_log(work_assignment_id: str) -> str:
|
|
101
|
+
"""Read the most recent Phase-1 build log for *work_assignment_id*.
|
|
102
|
+
|
|
103
|
+
The TUI writes ``~/.coord/test-build-<id>.log`` when the user presses B
|
|
104
|
+
to run ``coord test``. Returns a placeholder when the file doesn't exist
|
|
105
|
+
(user hasn't run the build yet) or can't be read. Clamped to
|
|
106
|
+
``MAX_BUILD_LOG_LINES`` lines.
|
|
107
|
+
"""
|
|
108
|
+
log_path = Path.home() / ".coord" / f"test-build-{work_assignment_id}.log"
|
|
109
|
+
try:
|
|
110
|
+
text = log_path.read_text(encoding="utf-8", errors="replace")
|
|
111
|
+
except (OSError, FileNotFoundError):
|
|
112
|
+
return "(no build log — press B in the TUI to run `coord test` first)"
|
|
113
|
+
lines = text.splitlines()
|
|
114
|
+
if len(lines) > MAX_BUILD_LOG_LINES:
|
|
115
|
+
truncated = len(lines) - MAX_BUILD_LOG_LINES
|
|
116
|
+
lines = lines[:MAX_BUILD_LOG_LINES]
|
|
117
|
+
lines.append(f"…[{truncated} more log lines truncated]")
|
|
118
|
+
return "\n".join(lines) if lines else "(build log empty)"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _read_claude_md(repo_root: Path) -> str:
|
|
122
|
+
"""Read the repo's top-level CLAUDE.md, clamped to ``MAX_CLAUDE_MD_CHARS``."""
|
|
123
|
+
path = repo_root / "CLAUDE.md"
|
|
124
|
+
try:
|
|
125
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
126
|
+
except (OSError, FileNotFoundError):
|
|
127
|
+
return ""
|
|
128
|
+
if len(text) > MAX_CLAUDE_MD_CHARS:
|
|
129
|
+
return text[:MAX_CLAUDE_MD_CHARS] + "\n…[truncated]"
|
|
130
|
+
return text
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_test_briefing(
|
|
134
|
+
*,
|
|
135
|
+
work_assignment_id: str,
|
|
136
|
+
issue_number: int,
|
|
137
|
+
issue_title: str,
|
|
138
|
+
repo_slug: str,
|
|
139
|
+
branch: str | None,
|
|
140
|
+
smoke_tests: list[str] | None,
|
|
141
|
+
run_cmd: str | None,
|
|
142
|
+
diff: str,
|
|
143
|
+
build_log: str,
|
|
144
|
+
claude_md: str,
|
|
145
|
+
) -> str:
|
|
146
|
+
"""Compose the seed briefing the test-chat worker sees as its first message.
|
|
147
|
+
|
|
148
|
+
The agent's ``TEST_CHAT_SYSTEM_PROMPT`` already tells it how to behave;
|
|
149
|
+
this packs the per-PR context so the assistant can give specific guidance.
|
|
150
|
+
"""
|
|
151
|
+
parts: list[str] = []
|
|
152
|
+
parts.append(
|
|
153
|
+
f"=== Test-chat context for {repo_slug}#{issue_number}: {issue_title} ===\n"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# PR diff — clamp to MAX_DIFF_LINES even when the caller already fetched
|
|
157
|
+
# a bounded diff, as a defensive second cap in case large diffs slip through.
|
|
158
|
+
diff_lines = diff.splitlines() if diff else []
|
|
159
|
+
if len(diff_lines) > MAX_DIFF_LINES:
|
|
160
|
+
truncated = len(diff_lines) - MAX_DIFF_LINES
|
|
161
|
+
diff_lines = diff_lines[:MAX_DIFF_LINES]
|
|
162
|
+
diff_lines.append(f"…[{truncated} more diff lines truncated]")
|
|
163
|
+
diff_clamped = "\n".join(diff_lines)
|
|
164
|
+
parts.append("PR DIFF:")
|
|
165
|
+
parts.append(diff_clamped.strip() if diff_clamped.strip() else "(no diff available)")
|
|
166
|
+
parts.append("")
|
|
167
|
+
|
|
168
|
+
# Build log.
|
|
169
|
+
parts.append("MOST RECENT BUILD LOG:")
|
|
170
|
+
parts.append(build_log.strip() if build_log.strip() else "(no build log)")
|
|
171
|
+
parts.append("")
|
|
172
|
+
|
|
173
|
+
# Smoke-test bullets from the worker.
|
|
174
|
+
parts.append("WORKER SMOKE_TESTS:")
|
|
175
|
+
if smoke_tests is None:
|
|
176
|
+
parts.append("(worker did not emit a SMOKE_TESTS block)")
|
|
177
|
+
elif len(smoke_tests) == 0:
|
|
178
|
+
parts.append("(none — worker reported change is internal)")
|
|
179
|
+
else:
|
|
180
|
+
for bullet in smoke_tests:
|
|
181
|
+
parts.append(f"- {bullet}")
|
|
182
|
+
parts.append("")
|
|
183
|
+
|
|
184
|
+
# App launch command.
|
|
185
|
+
parts.append("RUN COMMAND:")
|
|
186
|
+
parts.append(run_cmd.strip() if run_cmd and run_cmd.strip() else "(not configured)")
|
|
187
|
+
parts.append("")
|
|
188
|
+
|
|
189
|
+
# Project CLAUDE.md.
|
|
190
|
+
parts.append("PROJECT CLAUDE.md:")
|
|
191
|
+
parts.append(claude_md.strip() if claude_md.strip() else "(not found)")
|
|
192
|
+
parts.append("")
|
|
193
|
+
|
|
194
|
+
parts.append("---")
|
|
195
|
+
parts.append(
|
|
196
|
+
"The developer will now ask you questions about how to test this change. "
|
|
197
|
+
"Help them understand what to verify, which smoke-test bullets to prioritise, "
|
|
198
|
+
"and how to interpret build output. Use Read and Bash (read-only) to "
|
|
199
|
+
"inspect the repo when helpful. Keep replies short."
|
|
200
|
+
)
|
|
201
|
+
return "\n".join(parts)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def dispatch_test_chat(
|
|
205
|
+
*,
|
|
206
|
+
cfg: Config,
|
|
207
|
+
repo_cfg: Repo,
|
|
208
|
+
repo: str,
|
|
209
|
+
work_assignment_id: str,
|
|
210
|
+
machine_override: str | None = None,
|
|
211
|
+
) -> tuple[str, str]:
|
|
212
|
+
"""End-to-end: pick a machine, seed the briefing, dispatch a test-chat
|
|
213
|
+
assignment. Returns ``(assignment_id, machine_name)``.
|
|
214
|
+
|
|
215
|
+
Raises ``RuntimeError`` when no machine claims the repo, when the
|
|
216
|
+
work assignment can't be found in the DB, or when the agent rejects
|
|
217
|
+
the dispatch.
|
|
218
|
+
"""
|
|
219
|
+
# Pick the machine.
|
|
220
|
+
if machine_override:
|
|
221
|
+
machine = next(
|
|
222
|
+
(m for m in cfg.machines if m.name == machine_override),
|
|
223
|
+
None,
|
|
224
|
+
)
|
|
225
|
+
if machine is None:
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
f"machine {machine_override!r} not in coordinator.yml"
|
|
228
|
+
)
|
|
229
|
+
if not machine.can_work_on(repo):
|
|
230
|
+
raise RuntimeError(
|
|
231
|
+
f"machine {machine_override!r} does not list repo {repo!r}"
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
picked = pick_test_chat_machine(cfg, repo)
|
|
235
|
+
if picked is None:
|
|
236
|
+
raise RuntimeError(
|
|
237
|
+
f"no machine claims repo {repo!r} — test-chat needs a "
|
|
238
|
+
f"machine that has the repo cloned"
|
|
239
|
+
)
|
|
240
|
+
machine = picked
|
|
241
|
+
|
|
242
|
+
repo_path = machine.repo_path(repo)
|
|
243
|
+
if repo_path is None:
|
|
244
|
+
raise RuntimeError(
|
|
245
|
+
f"machine {machine.name!r} has no resolved path for repo {repo!r}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Look up the work assignment from the DB.
|
|
249
|
+
from coord.db import get_connection # noqa: PLC0415
|
|
250
|
+
|
|
251
|
+
conn = get_connection()
|
|
252
|
+
row = conn.execute(
|
|
253
|
+
"SELECT issue_number, issue_title, branch, smoke_tests "
|
|
254
|
+
"FROM assignments WHERE assignment_id=?",
|
|
255
|
+
(work_assignment_id,),
|
|
256
|
+
).fetchone()
|
|
257
|
+
if row is None:
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"work assignment {work_assignment_id!r} not found in DB"
|
|
260
|
+
)
|
|
261
|
+
issue_number: int = row["issue_number"]
|
|
262
|
+
issue_title: str = row["issue_title"]
|
|
263
|
+
branch: str | None = row["branch"]
|
|
264
|
+
|
|
265
|
+
# Decode smoke_tests JSON column.
|
|
266
|
+
smoke_tests: list[str] | None = None
|
|
267
|
+
raw_smoke = row["smoke_tests"]
|
|
268
|
+
if raw_smoke is not None:
|
|
269
|
+
try:
|
|
270
|
+
decoded = json.loads(raw_smoke)
|
|
271
|
+
smoke_tests = decoded if isinstance(decoded, list) else None
|
|
272
|
+
except (ValueError, TypeError):
|
|
273
|
+
smoke_tests = None
|
|
274
|
+
|
|
275
|
+
# Gather diff, build log, run_cmd, and CLAUDE.md.
|
|
276
|
+
repo_root = Path(repo_path).expanduser()
|
|
277
|
+
diff = _fetch_diff(branch, repo_root) if branch else "(no branch recorded)"
|
|
278
|
+
build_log = _read_build_log(work_assignment_id)
|
|
279
|
+
run_cmd = repo_cfg.run_cmd
|
|
280
|
+
claude_md = _read_claude_md(repo_root)
|
|
281
|
+
|
|
282
|
+
briefing = build_test_briefing(
|
|
283
|
+
work_assignment_id=work_assignment_id,
|
|
284
|
+
issue_number=issue_number,
|
|
285
|
+
issue_title=issue_title,
|
|
286
|
+
repo_slug=repo_cfg.github,
|
|
287
|
+
branch=branch,
|
|
288
|
+
smoke_tests=smoke_tests,
|
|
289
|
+
run_cmd=run_cmd,
|
|
290
|
+
diff=diff,
|
|
291
|
+
build_log=build_log,
|
|
292
|
+
claude_md=claude_md,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Resolve model and branch for the agent payload.
|
|
296
|
+
resolved_model = cfg.models.default
|
|
297
|
+
wire_model = cfg.models.resolve(resolved_model)
|
|
298
|
+
default_branch = repo_cfg.default_branch or "main"
|
|
299
|
+
|
|
300
|
+
# Coordinator-only files the worker must not touch.
|
|
301
|
+
files_forbidden: list[str] = list(repo_cfg.coordinator_only_files)
|
|
302
|
+
|
|
303
|
+
# POST directly to the agent so we can inject the test-chat–specific
|
|
304
|
+
# deny_commands (not present on the Proposal dataclass). This mirrors
|
|
305
|
+
# the pattern used by review.py.
|
|
306
|
+
url = f"http://{machine.host}:{AGENT_PORT}/assign"
|
|
307
|
+
payload: dict = {
|
|
308
|
+
"repo_name": repo,
|
|
309
|
+
"repo_path": repo_path,
|
|
310
|
+
"issue_number": issue_number,
|
|
311
|
+
"issue_title": issue_title,
|
|
312
|
+
"briefing": briefing,
|
|
313
|
+
"files_allowed": [],
|
|
314
|
+
"files_forbidden": files_forbidden,
|
|
315
|
+
"pull_repos": [],
|
|
316
|
+
"deny_commands": _TEST_CHAT_DENY_COMMANDS,
|
|
317
|
+
"model": wire_model,
|
|
318
|
+
"type": "test-chat",
|
|
319
|
+
"branch": default_branch,
|
|
320
|
+
}
|
|
321
|
+
try:
|
|
322
|
+
resp = httpx.post(url, json=payload, timeout=15)
|
|
323
|
+
resp.raise_for_status()
|
|
324
|
+
agent_response = resp.json()
|
|
325
|
+
except (httpx.HTTPError, httpx.TimeoutException) as exc:
|
|
326
|
+
raise RuntimeError(f"agent rejected test-chat dispatch: {exc}") from exc
|
|
327
|
+
|
|
328
|
+
assignment_id: str = agent_response.get("id") or uuid.uuid4().hex[:12]
|
|
329
|
+
|
|
330
|
+
# Persist the assignment row so the TUI can find it by polling the DB.
|
|
331
|
+
asg = Assignment(
|
|
332
|
+
machine_name=machine.name,
|
|
333
|
+
repo_name=repo,
|
|
334
|
+
issue_number=issue_number,
|
|
335
|
+
issue_title=issue_title,
|
|
336
|
+
files_allowed=[],
|
|
337
|
+
files_forbidden=files_forbidden,
|
|
338
|
+
briefing=briefing,
|
|
339
|
+
assignment_id=assignment_id,
|
|
340
|
+
status="running",
|
|
341
|
+
dispatched_at=time.time(),
|
|
342
|
+
type="test-chat",
|
|
343
|
+
model=resolved_model,
|
|
344
|
+
)
|
|
345
|
+
from coord.state import record_dispatched_assignment # noqa: PLC0415
|
|
346
|
+
|
|
347
|
+
record_dispatched_assignment(
|
|
348
|
+
assignment=asg,
|
|
349
|
+
repo_github=repo_cfg.github,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return assignment_id, machine.name
|