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/refine_chat.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""#264: dispatch a `type="refinement"` chat session for an issue.
|
|
2
|
+
|
|
3
|
+
Used by `coord refine-chat <repo> <issue>` (the CLI command shelled out by the
|
|
4
|
+
TUI's right-click `Refine with chat` action). Builds a seed briefing that
|
|
5
|
+
pre-loads the worker with the issue body, recent comments, the repo's
|
|
6
|
+
`CLAUDE.md`, and a bounded file-tree snapshot — same context shape a normal
|
|
7
|
+
worker briefing would carry, scoped down to read-only refinement use.
|
|
8
|
+
|
|
9
|
+
Also used by `coord refine-board <repo>` (#316 Phase C) for a board-level
|
|
10
|
+
(no-issue) refinement chat where the developer explores ideas without being
|
|
11
|
+
tied to a specific issue. These use ``issue_number=0`` as the sentinel value.
|
|
12
|
+
|
|
13
|
+
The session itself runs as a `type="refinement"` assignment on an agent
|
|
14
|
+
server. Tools are restricted to `Read` (see `coord/agent.py`); the developer
|
|
15
|
+
drives the conversation via `POST /inject/{id}` from the TUI.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
import subprocess
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from coord import github_ops
|
|
26
|
+
from coord.config import Config
|
|
27
|
+
from coord.models import Machine, Proposal, Repo
|
|
28
|
+
|
|
29
|
+
# Soft caps so the seed briefing stays bounded — even on repos with sprawling
|
|
30
|
+
# trees or chatty issues. These translate to <50 KiB of prompt context in
|
|
31
|
+
# typical use, which the 1h-ephemeral cache then makes nearly free on reopen.
|
|
32
|
+
MAX_COMMENT_BODIES = 5
|
|
33
|
+
MAX_COMMENT_BODY_CHARS = 800
|
|
34
|
+
MAX_FILE_TREE_LINES = 200
|
|
35
|
+
MAX_FILE_TREE_DEPTH = 2
|
|
36
|
+
MAX_CLAUDE_MD_CHARS = 8000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pick_refinement_machine(cfg: Config, repo: str) -> Machine | None:
|
|
40
|
+
"""Pick a machine to run the refinement session on.
|
|
41
|
+
|
|
42
|
+
Returns the first reachable machine that lists *repo* and is not
|
|
43
|
+
paused (via `coord pause <name>`). Refinement is read-only and
|
|
44
|
+
short-lived, so we don't need the freshness / capacity weighting
|
|
45
|
+
`coord plan` uses for work dispatch — any qualified machine works.
|
|
46
|
+
Returns `None` when no machine claims the repo.
|
|
47
|
+
"""
|
|
48
|
+
from coord.machine_pause import paused_set
|
|
49
|
+
paused = paused_set(cfg.machines)
|
|
50
|
+
for m in cfg.machines:
|
|
51
|
+
if (
|
|
52
|
+
m.can_work_on(repo)
|
|
53
|
+
and m.repo_path(repo) is not None
|
|
54
|
+
and m.name not in paused
|
|
55
|
+
):
|
|
56
|
+
return m
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fetch_recent_comments(slug: str, issue_number: int) -> list[dict]:
|
|
61
|
+
"""Best-effort fetch of an issue's recent comments via `gh`.
|
|
62
|
+
|
|
63
|
+
Returns an empty list on any failure — refinement still works without
|
|
64
|
+
comment context, the seed just gets a "(none)" stub.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
raw = github_ops._gh(
|
|
68
|
+
"issue", "view", str(issue_number),
|
|
69
|
+
"--repo", slug,
|
|
70
|
+
"--json", "comments",
|
|
71
|
+
)
|
|
72
|
+
data = json.loads(raw)
|
|
73
|
+
comments = data.get("comments") or []
|
|
74
|
+
if not isinstance(comments, list):
|
|
75
|
+
return []
|
|
76
|
+
return comments[-MAX_COMMENT_BODIES:]
|
|
77
|
+
except (RuntimeError, ValueError, json.JSONDecodeError):
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _read_claude_md(repo_root: Path) -> str:
|
|
82
|
+
"""Read the repo's `CLAUDE.md` (top-level) and clamp to MAX_CLAUDE_MD_CHARS.
|
|
83
|
+
|
|
84
|
+
Returns an empty string when the file doesn't exist or is unreadable.
|
|
85
|
+
"""
|
|
86
|
+
path = repo_root / "CLAUDE.md"
|
|
87
|
+
try:
|
|
88
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
89
|
+
except (OSError, FileNotFoundError):
|
|
90
|
+
return ""
|
|
91
|
+
if len(text) > MAX_CLAUDE_MD_CHARS:
|
|
92
|
+
return text[:MAX_CLAUDE_MD_CHARS] + "\n…[truncated]"
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _file_tree_snapshot(repo_root: Path) -> str:
|
|
97
|
+
"""Capture a shallow file-tree snapshot for the seed.
|
|
98
|
+
|
|
99
|
+
Uses `find` with a depth cap and clamps the output to
|
|
100
|
+
MAX_FILE_TREE_LINES. Excludes the obvious noise (`.git`, build dirs,
|
|
101
|
+
deps) so the snapshot reads as a project map rather than a `node_modules`
|
|
102
|
+
dump. Returns "(no tree available)" when the shell-out fails.
|
|
103
|
+
"""
|
|
104
|
+
# Use `-prune` so the excluded directories disappear from the listing
|
|
105
|
+
# entirely (not just their contents) — `target/` showing up as a bare
|
|
106
|
+
# line with no children reads as noise, not signal.
|
|
107
|
+
excluded = (".git", "target", "node_modules", ".venv", "__pycache__")
|
|
108
|
+
prune_args: list[str] = []
|
|
109
|
+
for i, name in enumerate(excluded):
|
|
110
|
+
if i > 0:
|
|
111
|
+
prune_args.extend(["-o"])
|
|
112
|
+
prune_args.extend(["-name", name])
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[
|
|
116
|
+
"find", str(repo_root),
|
|
117
|
+
"-maxdepth", str(MAX_FILE_TREE_DEPTH),
|
|
118
|
+
"(", *prune_args, ")", "-prune",
|
|
119
|
+
"-o", "-print",
|
|
120
|
+
],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=10,
|
|
124
|
+
)
|
|
125
|
+
if result.returncode != 0:
|
|
126
|
+
return "(no tree available)"
|
|
127
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
128
|
+
return "(no tree available)"
|
|
129
|
+
lines = result.stdout.splitlines()
|
|
130
|
+
# Drop the repo root itself for readability, then make paths repo-relative.
|
|
131
|
+
rel: list[str] = []
|
|
132
|
+
root_str = str(repo_root)
|
|
133
|
+
for line in lines:
|
|
134
|
+
if not line or line == root_str:
|
|
135
|
+
continue
|
|
136
|
+
rel.append(line[len(root_str) + 1:] if line.startswith(root_str + "/") else line)
|
|
137
|
+
rel.sort()
|
|
138
|
+
if len(rel) > MAX_FILE_TREE_LINES:
|
|
139
|
+
truncated = len(rel) - MAX_FILE_TREE_LINES
|
|
140
|
+
rel = rel[:MAX_FILE_TREE_LINES]
|
|
141
|
+
rel.append(f"…[{truncated} more entries truncated]")
|
|
142
|
+
return "\n".join(rel) if rel else "(empty repo)"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_refinement_briefing(
|
|
146
|
+
*,
|
|
147
|
+
repo_slug: str,
|
|
148
|
+
issue_number: int,
|
|
149
|
+
issue_title: str,
|
|
150
|
+
issue_body: str,
|
|
151
|
+
comments: list[dict],
|
|
152
|
+
claude_md: str,
|
|
153
|
+
file_tree: str,
|
|
154
|
+
) -> str:
|
|
155
|
+
"""Compose the seed briefing the worker sees as its first user message.
|
|
156
|
+
|
|
157
|
+
The agent's `REFINEMENT_SYSTEM_PROMPT` already tells it how to behave;
|
|
158
|
+
this just packs the conversation context.
|
|
159
|
+
"""
|
|
160
|
+
parts: list[str] = []
|
|
161
|
+
parts.append(f"=== Refinement context for {repo_slug}#{issue_number}: {issue_title} ===\n")
|
|
162
|
+
parts.append("ISSUE BODY:")
|
|
163
|
+
parts.append(issue_body.strip() if issue_body.strip() else "(empty)")
|
|
164
|
+
parts.append("")
|
|
165
|
+
parts.append("RECENT COMMENTS:")
|
|
166
|
+
if comments:
|
|
167
|
+
for c in comments:
|
|
168
|
+
author = (c.get("author") or {}).get("login") or "?"
|
|
169
|
+
body = (c.get("body") or "").strip()
|
|
170
|
+
if len(body) > MAX_COMMENT_BODY_CHARS:
|
|
171
|
+
body = body[:MAX_COMMENT_BODY_CHARS] + "…[truncated]"
|
|
172
|
+
parts.append(f"--- @{author}")
|
|
173
|
+
parts.append(body if body else "(empty)")
|
|
174
|
+
else:
|
|
175
|
+
parts.append("(none)")
|
|
176
|
+
parts.append("")
|
|
177
|
+
parts.append("PROJECT CLAUDE.md:")
|
|
178
|
+
parts.append(claude_md.strip() if claude_md.strip() else "(not found)")
|
|
179
|
+
parts.append("")
|
|
180
|
+
parts.append("REPO FILE TREE (depth 2, common build dirs excluded):")
|
|
181
|
+
parts.append(file_tree)
|
|
182
|
+
parts.append("")
|
|
183
|
+
parts.append("---")
|
|
184
|
+
parts.append(
|
|
185
|
+
"The developer will now ask you questions about this issue. "
|
|
186
|
+
"Ask focused clarifying questions back. Use the Read tool to peek at "
|
|
187
|
+
"files when the conversation calls for it. Keep replies short."
|
|
188
|
+
)
|
|
189
|
+
return "\n".join(parts)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def dispatch_refinement(
|
|
193
|
+
*,
|
|
194
|
+
cfg: Config,
|
|
195
|
+
repo_cfg: Repo,
|
|
196
|
+
repo: str,
|
|
197
|
+
issue_number: int,
|
|
198
|
+
machine_override: str | None = None,
|
|
199
|
+
) -> tuple[str, str]:
|
|
200
|
+
"""End-to-end: pick a machine, seed the briefing, dispatch a refinement
|
|
201
|
+
assignment. Returns ``(assignment_id, machine_name)``.
|
|
202
|
+
|
|
203
|
+
Raises ``RuntimeError`` when no machine claims the repo, when the issue
|
|
204
|
+
can't be fetched, or when the agent rejects the dispatch.
|
|
205
|
+
"""
|
|
206
|
+
# Pick the machine.
|
|
207
|
+
if machine_override:
|
|
208
|
+
machine = next(
|
|
209
|
+
(m for m in cfg.machines if m.name == machine_override),
|
|
210
|
+
None,
|
|
211
|
+
)
|
|
212
|
+
if machine is None:
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
f"machine {machine_override!r} not in coordinator.yml"
|
|
215
|
+
)
|
|
216
|
+
if not machine.can_work_on(repo):
|
|
217
|
+
raise RuntimeError(
|
|
218
|
+
f"machine {machine_override!r} does not list repo {repo!r}"
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
picked = pick_refinement_machine(cfg, repo)
|
|
222
|
+
if picked is None:
|
|
223
|
+
raise RuntimeError(
|
|
224
|
+
f"no machine claims repo {repo!r} — refinement needs a "
|
|
225
|
+
f"machine that has the repo cloned to read CLAUDE.md"
|
|
226
|
+
)
|
|
227
|
+
machine = picked
|
|
228
|
+
|
|
229
|
+
repo_path = machine.repo_path(repo)
|
|
230
|
+
if repo_path is None:
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
f"machine {machine.name!r} has no resolved path for repo {repo!r}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Fetch the issue + recent comments.
|
|
236
|
+
try:
|
|
237
|
+
issue_data = github_ops.get_issue(repo_cfg.github, issue_number)
|
|
238
|
+
except RuntimeError as exc:
|
|
239
|
+
raise RuntimeError(f"could not fetch issue #{issue_number}: {exc}") from exc
|
|
240
|
+
issue_title = issue_data.get("title") or f"Issue #{issue_number}"
|
|
241
|
+
issue_body = issue_data.get("body") or ""
|
|
242
|
+
comments = _fetch_recent_comments(repo_cfg.github, issue_number)
|
|
243
|
+
|
|
244
|
+
# Read the LOCAL CLAUDE.md + file tree (from the coordinator's view of the
|
|
245
|
+
# repo, which may differ from the agent's checkout — but it's the source
|
|
246
|
+
# of truth for project conventions in the seed).
|
|
247
|
+
repo_root = Path(repo_path).expanduser()
|
|
248
|
+
claude_md = _read_claude_md(repo_root)
|
|
249
|
+
file_tree = _file_tree_snapshot(repo_root)
|
|
250
|
+
|
|
251
|
+
briefing = build_refinement_briefing(
|
|
252
|
+
repo_slug=repo_cfg.github,
|
|
253
|
+
issue_number=issue_number,
|
|
254
|
+
issue_title=issue_title,
|
|
255
|
+
issue_body=issue_body,
|
|
256
|
+
comments=comments,
|
|
257
|
+
claude_md=claude_md,
|
|
258
|
+
file_tree=file_tree,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Build the Proposal — model resolves through models.versions so the
|
|
262
|
+
# wire payload carries an exact id (the same path work dispatch uses).
|
|
263
|
+
# #1430: deliberately not consulting models.labels — this is a
|
|
264
|
+
# conversational refinement chat, often used to help shape the issue
|
|
265
|
+
# (including its labels) in the first place, not the eventual work
|
|
266
|
+
# dispatch that follows.
|
|
267
|
+
resolved_model = cfg.models.default
|
|
268
|
+
proposal = Proposal(
|
|
269
|
+
id=0,
|
|
270
|
+
machine_name=machine.name,
|
|
271
|
+
repo_name=repo,
|
|
272
|
+
issue_number=issue_number,
|
|
273
|
+
issue_title=issue_title,
|
|
274
|
+
rationale="refine-chat",
|
|
275
|
+
briefing=briefing,
|
|
276
|
+
model=resolved_model,
|
|
277
|
+
type="refinement",
|
|
278
|
+
required_gates=[],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
from coord.dispatch import dispatch_with_retry
|
|
282
|
+
from coord.state import record_dispatched_assignment
|
|
283
|
+
from coord.models import Assignment
|
|
284
|
+
|
|
285
|
+
response = dispatch_with_retry(
|
|
286
|
+
proposal, cfg,
|
|
287
|
+
max_retries=cfg.concurrency.max_retries,
|
|
288
|
+
backoff_base=cfg.concurrency.backoff_base,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
assignment_id = response.get("id") or uuid.uuid4().hex[:12]
|
|
292
|
+
|
|
293
|
+
# Persist the assignment row so the TUI can find it by polling the DB.
|
|
294
|
+
asg = Assignment(
|
|
295
|
+
machine_name=machine.name,
|
|
296
|
+
repo_name=repo,
|
|
297
|
+
issue_number=issue_number,
|
|
298
|
+
issue_title=issue_title,
|
|
299
|
+
files_allowed=[],
|
|
300
|
+
files_forbidden=[],
|
|
301
|
+
briefing=briefing,
|
|
302
|
+
assignment_id=assignment_id,
|
|
303
|
+
status="running",
|
|
304
|
+
dispatched_at=time.time(),
|
|
305
|
+
type="refinement",
|
|
306
|
+
model=resolved_model,
|
|
307
|
+
)
|
|
308
|
+
record_dispatched_assignment(
|
|
309
|
+
assignment=asg,
|
|
310
|
+
repo_github=repo_cfg.github,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return assignment_id, machine.name
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ─── #316 Phase C: board-level (no-issue) refinement chat ─────────────────────
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def build_board_refinement_briefing(
|
|
320
|
+
*,
|
|
321
|
+
repo_slug: str,
|
|
322
|
+
claude_md: str,
|
|
323
|
+
file_tree: str,
|
|
324
|
+
open_issue_titles: list[str],
|
|
325
|
+
) -> str:
|
|
326
|
+
"""Compose the seed briefing for a board-level refinement chat.
|
|
327
|
+
|
|
328
|
+
Unlike the issue-specific refinement, there is no issue body or comments —
|
|
329
|
+
the session is open-ended exploration. The worker is seeded with the repo's
|
|
330
|
+
CLAUDE.md, the file tree, and the current open issue titles (for awareness
|
|
331
|
+
of what's already tracked) so the developer can brainstorm freely.
|
|
332
|
+
"""
|
|
333
|
+
parts: list[str] = []
|
|
334
|
+
parts.append(f"=== Board-level refinement for {repo_slug} ===\n")
|
|
335
|
+
parts.append("This is an open-ended brainstorming session for the repository.")
|
|
336
|
+
parts.append("There is no specific issue — the developer wants to explore ideas,")
|
|
337
|
+
parts.append("discuss the codebase, or draft new work items.\n")
|
|
338
|
+
parts.append("PROJECT CLAUDE.md:")
|
|
339
|
+
parts.append(claude_md.strip() if claude_md.strip() else "(not found)")
|
|
340
|
+
parts.append("")
|
|
341
|
+
parts.append("REPO FILE TREE (depth 2, common build dirs excluded):")
|
|
342
|
+
parts.append(file_tree)
|
|
343
|
+
parts.append("")
|
|
344
|
+
if open_issue_titles:
|
|
345
|
+
parts.append("CURRENT OPEN ISSUES (for context — avoid duplicates):")
|
|
346
|
+
for title in open_issue_titles:
|
|
347
|
+
parts.append(f" - {title}")
|
|
348
|
+
else:
|
|
349
|
+
parts.append("CURRENT OPEN ISSUES: (none)")
|
|
350
|
+
parts.append("")
|
|
351
|
+
parts.append("---")
|
|
352
|
+
parts.append(
|
|
353
|
+
"The developer will start the conversation. Ask focused questions, "
|
|
354
|
+
"explore ideas, and help articulate intent. Use the Read tool to peek "
|
|
355
|
+
"at files when relevant. Keep replies concise."
|
|
356
|
+
)
|
|
357
|
+
return "\n".join(parts)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def dispatch_board_refinement(
|
|
361
|
+
*,
|
|
362
|
+
cfg: Config,
|
|
363
|
+
repo: str,
|
|
364
|
+
machine_override: str | None = None,
|
|
365
|
+
) -> tuple[str, str]:
|
|
366
|
+
"""Dispatch a board-level (no-issue) refinement chat for *repo*.
|
|
367
|
+
|
|
368
|
+
Uses ``issue_number=0`` as the sentinel value so the TUI's
|
|
369
|
+
``chat_is_board_chat()`` can distinguish board chats from issue-specific
|
|
370
|
+
ones. Returns ``(assignment_id, machine_name)``.
|
|
371
|
+
|
|
372
|
+
Raises ``RuntimeError`` when the repo is unknown, no machine claims it,
|
|
373
|
+
or the agent rejects the dispatch.
|
|
374
|
+
"""
|
|
375
|
+
repo_cfg = cfg.repo(repo)
|
|
376
|
+
if repo_cfg is None:
|
|
377
|
+
raise RuntimeError(f"repo {repo!r} not in coordinator.yml")
|
|
378
|
+
|
|
379
|
+
# Pick the machine.
|
|
380
|
+
if machine_override:
|
|
381
|
+
machine = next(
|
|
382
|
+
(m for m in cfg.machines if m.name == machine_override),
|
|
383
|
+
None,
|
|
384
|
+
)
|
|
385
|
+
if machine is None:
|
|
386
|
+
raise RuntimeError(f"machine {machine_override!r} not in coordinator.yml")
|
|
387
|
+
if not machine.can_work_on(repo):
|
|
388
|
+
raise RuntimeError(
|
|
389
|
+
f"machine {machine_override!r} does not list repo {repo!r}"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
picked = pick_refinement_machine(cfg, repo)
|
|
393
|
+
if picked is None:
|
|
394
|
+
raise RuntimeError(
|
|
395
|
+
f"no machine claims repo {repo!r} — refine-board needs a "
|
|
396
|
+
"machine that has the repo cloned to read CLAUDE.md"
|
|
397
|
+
)
|
|
398
|
+
machine = picked
|
|
399
|
+
|
|
400
|
+
repo_path = machine.repo_path(repo)
|
|
401
|
+
if repo_path is None:
|
|
402
|
+
raise RuntimeError(
|
|
403
|
+
f"machine {machine.name!r} has no resolved path for repo {repo!r}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
repo_root = Path(repo_path).expanduser()
|
|
407
|
+
claude_md = _read_claude_md(repo_root)
|
|
408
|
+
file_tree = _file_tree_snapshot(repo_root)
|
|
409
|
+
|
|
410
|
+
# Fetch open issue titles for near-duplicate awareness (best-effort).
|
|
411
|
+
open_titles: list[str] = []
|
|
412
|
+
try:
|
|
413
|
+
raw = github_ops._gh(
|
|
414
|
+
"issue", "list",
|
|
415
|
+
"--repo", repo_cfg.github,
|
|
416
|
+
"--state", "open",
|
|
417
|
+
"--limit", "20",
|
|
418
|
+
"--json", "title",
|
|
419
|
+
)
|
|
420
|
+
data = json.loads(raw)
|
|
421
|
+
open_titles = [item.get("title", "") for item in data if item.get("title")]
|
|
422
|
+
except Exception:
|
|
423
|
+
pass # Not critical — session still useful without the list.
|
|
424
|
+
|
|
425
|
+
briefing = build_board_refinement_briefing(
|
|
426
|
+
repo_slug=repo_cfg.github,
|
|
427
|
+
claude_md=claude_md,
|
|
428
|
+
file_tree=file_tree,
|
|
429
|
+
open_issue_titles=open_titles,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# #1430: deliberately not consulting models.labels — a board-level
|
|
433
|
+
# refinement chat has no single issue (issue_number=0 sentinel below),
|
|
434
|
+
# so there's no label to resolve against.
|
|
435
|
+
resolved_model = cfg.models.default
|
|
436
|
+
issue_title = f"Board refinement for {repo}"
|
|
437
|
+
|
|
438
|
+
# #316: issue_number=0 is the sentinel for board-level chats. The TUI
|
|
439
|
+
# uses `w.issue_number == 0` to route to the Board Chat tab rather than
|
|
440
|
+
# the Pipeline Refinement tab.
|
|
441
|
+
from coord.dispatch import dispatch_with_retry
|
|
442
|
+
from coord.state import record_dispatched_assignment
|
|
443
|
+
from coord.models import Assignment, Proposal
|
|
444
|
+
|
|
445
|
+
proposal = Proposal(
|
|
446
|
+
id=0,
|
|
447
|
+
machine_name=machine.name,
|
|
448
|
+
repo_name=repo,
|
|
449
|
+
issue_number=0,
|
|
450
|
+
issue_title=issue_title,
|
|
451
|
+
rationale="refine-board",
|
|
452
|
+
briefing=briefing,
|
|
453
|
+
model=resolved_model,
|
|
454
|
+
type="refinement",
|
|
455
|
+
required_gates=[],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
response = dispatch_with_retry(
|
|
459
|
+
proposal, cfg,
|
|
460
|
+
max_retries=cfg.concurrency.max_retries,
|
|
461
|
+
backoff_base=cfg.concurrency.backoff_base,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
assignment_id = response.get("id") or uuid.uuid4().hex[:12]
|
|
465
|
+
|
|
466
|
+
asg = Assignment(
|
|
467
|
+
machine_name=machine.name,
|
|
468
|
+
repo_name=repo,
|
|
469
|
+
issue_number=0,
|
|
470
|
+
issue_title=issue_title,
|
|
471
|
+
files_allowed=[],
|
|
472
|
+
files_forbidden=[],
|
|
473
|
+
briefing=briefing,
|
|
474
|
+
assignment_id=assignment_id,
|
|
475
|
+
status="running",
|
|
476
|
+
dispatched_at=time.time(),
|
|
477
|
+
type="refinement",
|
|
478
|
+
model=resolved_model,
|
|
479
|
+
)
|
|
480
|
+
record_dispatched_assignment(
|
|
481
|
+
assignment=asg,
|
|
482
|
+
repo_github=repo_cfg.github,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return assignment_id, machine.name
|