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/milestone_chat.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""#770 (Phase 2 of #767) + #1009 + #1017: dispatch a `type="milestone-chat"`
|
|
2
|
+
session.
|
|
3
|
+
|
|
4
|
+
Lets the operator chat about a milestone's issues and — once they confirm —
|
|
5
|
+
write the `## Work order` block that ``coord/milestone_order.py`` (#768)
|
|
6
|
+
parses into a DAG/frontier. #1009 widened this beyond that original narrow
|
|
7
|
+
slice: the same propose-then-confirm-then-write discipline now also covers
|
|
8
|
+
creating a brand-new milestone (:func:`dispatch_new_milestone_chat`, no
|
|
9
|
+
tracking issue yet), editing an existing milestone's title/description/due
|
|
10
|
+
date, and assigning an issue to a milestone. #1017 adds a third seed mode —
|
|
11
|
+
an "add sub-issue" chat seeded with the epic body plus a candidate child
|
|
12
|
+
issue (:func:`build_milestone_chat_briefing`'s ``candidate_child_issue``
|
|
13
|
+
param) — now that #1008 shipped `coord milestone add-child`. See
|
|
14
|
+
``MILESTONE_CHAT_SYSTEM_PROMPT`` in ``coord/agent.py`` for the exact
|
|
15
|
+
commands the worker is allowed to run. Full #645 "milestone-steward" scope
|
|
16
|
+
(creating/editing the *issues themselves*) stays out of scope until that
|
|
17
|
+
lands; nothing here forecloses it.
|
|
18
|
+
|
|
19
|
+
Used by `coord milestone chat <repo> <tracking_issue>` (existing milestone,
|
|
20
|
+
optionally `--add-child <issue>` to seed the add-sub-issue mode) and
|
|
21
|
+
`coord milestone chat <repo> --new` (brand-new milestone) — both CLI entry
|
|
22
|
+
points live in ``coord/commands/milestone.py``.
|
|
23
|
+
|
|
24
|
+
The session itself runs as a `type="milestone-chat"` assignment on an agent
|
|
25
|
+
server. Tools are `Read,Bash` (see `coord/agent.py`); a deny list blocks raw
|
|
26
|
+
`gh` mutations and unrelated `coord` write commands — the write actions the
|
|
27
|
+
worker may take are `coord milestone write-order`, `create`, `edit`,
|
|
28
|
+
`assign`, and `add-child`, and only after the operator has explicitly
|
|
29
|
+
confirmed the specific proposed change in conversation.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import dataclasses
|
|
34
|
+
import time
|
|
35
|
+
import uuid
|
|
36
|
+
|
|
37
|
+
from coord import github_ops
|
|
38
|
+
from coord.config import Config
|
|
39
|
+
from coord.models import Machine, Proposal
|
|
40
|
+
from coord.refine_chat import pick_refinement_machine
|
|
41
|
+
|
|
42
|
+
# Soft caps so the seed briefing stays bounded on milestones with many/large
|
|
43
|
+
# issues — mirrors the caps `new_issue_chat.py` / `refine_chat.py` use.
|
|
44
|
+
MAX_ISSUES = 50
|
|
45
|
+
MAX_ISSUE_BODY_CHARS = 1500
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pick_milestone_chat_machine(cfg: Config, repo: str) -> Machine | None:
|
|
49
|
+
"""Pick a machine to run the milestone-chat session on.
|
|
50
|
+
|
|
51
|
+
Milestone-chat is read-only w.r.t. git (no worktree, no branch — its one
|
|
52
|
+
write action goes through `coord milestone write-order` against the
|
|
53
|
+
GitHub API, not the local checkout) and short-lived, so any qualified,
|
|
54
|
+
unpaused machine works — same selection `refine_chat`/`new_issue_chat`
|
|
55
|
+
use. Reused directly rather than re-implemented (identical logic).
|
|
56
|
+
"""
|
|
57
|
+
return pick_refinement_machine(cfg, repo)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fetch_milestone_issues(slug: str, milestone_number: int) -> list[dict]:
|
|
61
|
+
"""Best-effort fetch of open issues under *milestone_number*.
|
|
62
|
+
|
|
63
|
+
Returns a list of ``{"number", "title", "body"}`` dicts (bodies
|
|
64
|
+
truncated to ``MAX_ISSUE_BODY_CHARS`` so cohort/dependency inference has
|
|
65
|
+
signal without blowing the prompt budget), capped at ``MAX_ISSUES``.
|
|
66
|
+
Returns an empty list on any failure so the seed briefing still works
|
|
67
|
+
without issue context — the operator can still chat, just with less to
|
|
68
|
+
go on.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
issues = github_ops.get_open_issues(slug)
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
return []
|
|
74
|
+
under_milestone = [
|
|
75
|
+
i for i in issues
|
|
76
|
+
if (i.get("milestone") or {}).get("number") == milestone_number
|
|
77
|
+
]
|
|
78
|
+
out: list[dict] = []
|
|
79
|
+
for issue in under_milestone[:MAX_ISSUES]:
|
|
80
|
+
body = (issue.get("body") or "").strip()
|
|
81
|
+
if len(body) > MAX_ISSUE_BODY_CHARS:
|
|
82
|
+
body = body[:MAX_ISSUE_BODY_CHARS] + "\n...(truncated)"
|
|
83
|
+
out.append({
|
|
84
|
+
"number": issue.get("number"),
|
|
85
|
+
"title": issue.get("title") or "",
|
|
86
|
+
"body": body,
|
|
87
|
+
})
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_milestone_chat_briefing(
|
|
92
|
+
*,
|
|
93
|
+
repo_name: str,
|
|
94
|
+
repo_slug: str,
|
|
95
|
+
milestone_title: str,
|
|
96
|
+
tracking_issue_number: int,
|
|
97
|
+
tracking_issue_body: str,
|
|
98
|
+
issues: list[dict],
|
|
99
|
+
milestone_number: int | None = None,
|
|
100
|
+
milestone_description: str | None = None,
|
|
101
|
+
milestone_due_on: str | None = None,
|
|
102
|
+
candidate_child_issue: dict | None = None,
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Compose the seed briefing the worker sees as its first user message.
|
|
105
|
+
|
|
106
|
+
The agent's ``MILESTONE_CHAT_SYSTEM_PROMPT`` already tells it how to
|
|
107
|
+
behave; this packs the milestone context into the first user turn.
|
|
108
|
+
|
|
109
|
+
*milestone_number*/*milestone_description*/*milestone_due_on* are
|
|
110
|
+
optional (#1009) — best-effort metadata so an "edit this milestone"
|
|
111
|
+
conversation has the current values to propose changes against without
|
|
112
|
+
a separate lookup. Omitted (``None``) when the caller couldn't fetch
|
|
113
|
+
them; the chat still works, just without that convenience context.
|
|
114
|
+
|
|
115
|
+
*candidate_child_issue* (#1017) is an optional ``{"number", "title",
|
|
116
|
+
"body"}`` dict — when supplied (the TUI's "Add sub-issue via chat…"
|
|
117
|
+
entry, or `coord milestone chat --add-child`), the briefing calls out
|
|
118
|
+
that candidate and steers the conversation toward proposing a `coord
|
|
119
|
+
milestone add-child` splice rather than a generic discussion.
|
|
120
|
+
"""
|
|
121
|
+
parts: list[str] = []
|
|
122
|
+
parts.append(
|
|
123
|
+
f"=== Milestone chat context for {repo_slug} "
|
|
124
|
+
f"— milestone {milestone_title!r} ===\n"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if milestone_number is not None:
|
|
128
|
+
parts.append(f"MILESTONE NUMBER: {milestone_number}")
|
|
129
|
+
parts.append(
|
|
130
|
+
"CURRENT DESCRIPTION: "
|
|
131
|
+
+ (milestone_description.strip() if milestone_description and milestone_description.strip() else "(none)")
|
|
132
|
+
)
|
|
133
|
+
parts.append(f"CURRENT DUE DATE: {milestone_due_on or '(none)'}")
|
|
134
|
+
parts.append("")
|
|
135
|
+
|
|
136
|
+
parts.append(f"TRACKING ISSUE: #{tracking_issue_number}")
|
|
137
|
+
parts.append(
|
|
138
|
+
"CURRENT TRACKING ISSUE BODY (may already carry a `## Work order` "
|
|
139
|
+
"block from a prior run — treat re-runs as idempotent updates, not "
|
|
140
|
+
"duplicates):"
|
|
141
|
+
)
|
|
142
|
+
parts.append(tracking_issue_body.strip() if tracking_issue_body.strip() else "(empty)")
|
|
143
|
+
parts.append("")
|
|
144
|
+
|
|
145
|
+
parts.append(f"OPEN ISSUES UNDER THIS MILESTONE ({len(issues)}):")
|
|
146
|
+
if issues:
|
|
147
|
+
for issue in issues:
|
|
148
|
+
parts.append(f"--- #{issue['number']}: {issue['title']} ---")
|
|
149
|
+
parts.append(issue["body"] if issue["body"] else "(no body)")
|
|
150
|
+
parts.append("")
|
|
151
|
+
else:
|
|
152
|
+
parts.append(" (none fetched)")
|
|
153
|
+
|
|
154
|
+
parts.append("---")
|
|
155
|
+
parts.append(
|
|
156
|
+
"Discuss this milestone with the operator. When asked to propose a "
|
|
157
|
+
"work order — or when it's clearly useful — infer parallel cohorts "
|
|
158
|
+
"(`group: <label>`) vs. hard dependencies (`after: #N[,#M...]`) from "
|
|
159
|
+
"the issue bodies above, present the proposed `## Work order` block, "
|
|
160
|
+
"and only after the operator explicitly confirms it, write it with:\n\n"
|
|
161
|
+
f" coord milestone write-order {repo_name} {tracking_issue_number} "
|
|
162
|
+
"<<'EOF'\n"
|
|
163
|
+
" - #762 {group: A}\n"
|
|
164
|
+
" ...\n"
|
|
165
|
+
" EOF\n\n"
|
|
166
|
+
"(#1061: no `[ ]`/`[x]` checkbox — the grammar dropped it since it "
|
|
167
|
+
"was never read for readiness; the parser still accepts the old "
|
|
168
|
+
"checkbox form on bodies that already have one, but propose new "
|
|
169
|
+
"lines checkbox-free.)"
|
|
170
|
+
)
|
|
171
|
+
if milestone_number is not None:
|
|
172
|
+
parts.append(
|
|
173
|
+
"The operator may also ask you to edit this milestone's title/"
|
|
174
|
+
"description/due date, or assign an issue to it — propose the "
|
|
175
|
+
"exact change, and once confirmed run:\n\n"
|
|
176
|
+
f" coord milestone edit {repo_name} {milestone_number} "
|
|
177
|
+
"[--title '...'] [--description '...'] [--due-on <iso8601>]\n"
|
|
178
|
+
f" coord milestone assign {repo_name} <issue> {milestone_number}\n\n"
|
|
179
|
+
"Always single-quote title/description values (never double-"
|
|
180
|
+
"quote them) — single quotes stop the shell from expanding "
|
|
181
|
+
"`$(...)`, backticks, or `$VAR` in operator-supplied text before "
|
|
182
|
+
"`coord` sees it. If a value itself contains a single quote, "
|
|
183
|
+
"escape it as `'\\''` (close the quote, insert an escaped quote, "
|
|
184
|
+
"reopen), e.g. \"operator's plan\" becomes 'operator'\\''s plan'."
|
|
185
|
+
)
|
|
186
|
+
if candidate_child_issue is not None:
|
|
187
|
+
child_number = candidate_child_issue.get("number")
|
|
188
|
+
child_title = candidate_child_issue.get("title") or ""
|
|
189
|
+
child_body = candidate_child_issue.get("body") or "(no body)"
|
|
190
|
+
parts.append("")
|
|
191
|
+
parts.append(
|
|
192
|
+
f"ADD SUB-ISSUE MODE: the operator wants to discuss splicing "
|
|
193
|
+
f"#{child_number} onto this epic's `## Sub-issues` checklist "
|
|
194
|
+
"(#1008/#1017) as a child of the tracking issue above. Candidate "
|
|
195
|
+
"issue:"
|
|
196
|
+
)
|
|
197
|
+
parts.append(f"--- #{child_number}: {child_title} ---")
|
|
198
|
+
parts.append(child_body)
|
|
199
|
+
parts.append("")
|
|
200
|
+
parts.append(
|
|
201
|
+
"Discuss whether this candidate belongs under the epic, and if "
|
|
202
|
+
"so, whether it should carry a `{group: <label>}` cohort or a "
|
|
203
|
+
"`{after: #N[,#M...]}` hard dependency — ground any inference in "
|
|
204
|
+
"explicit signals in the issue bodies, the same way you would "
|
|
205
|
+
"for a `## Work order` entry. Present the exact splice you're "
|
|
206
|
+
"proposing, and only after the operator explicitly confirms it, "
|
|
207
|
+
"run:\n\n"
|
|
208
|
+
f" coord milestone add-child {repo_name} {tracking_issue_number} "
|
|
209
|
+
f"{child_number} [--group '<group>'] [--after <N>[,<M>...]]\n\n"
|
|
210
|
+
"Omit --group/--after entirely for a bare splice with no "
|
|
211
|
+
"annotation. To remove a sub-issue instead, run `coord milestone "
|
|
212
|
+
f"add-child {repo_name} {tracking_issue_number} {child_number} "
|
|
213
|
+
"--remove` (only after the operator confirms removal)."
|
|
214
|
+
)
|
|
215
|
+
return "\n".join(parts)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def build_new_milestone_chat_briefing(
|
|
219
|
+
*,
|
|
220
|
+
repo_name: str,
|
|
221
|
+
repo_slug: str,
|
|
222
|
+
seed_title: str | None,
|
|
223
|
+
seed_prompt: str | None,
|
|
224
|
+
) -> str:
|
|
225
|
+
"""Compose the seed briefing for a brand-new milestone (#1009) — no
|
|
226
|
+
tracking issue exists yet, so there's nothing to fetch from GitHub.
|
|
227
|
+
|
|
228
|
+
*seed_title*/*seed_prompt* are whatever the operator supplied when
|
|
229
|
+
starting the chat (e.g. from a TUI "New milestone… → Chat about this"
|
|
230
|
+
action); both are optional since the operator may want to start from a
|
|
231
|
+
blank conversation.
|
|
232
|
+
"""
|
|
233
|
+
parts: list[str] = []
|
|
234
|
+
parts.append(f"=== New-milestone chat context for {repo_slug} ===\n")
|
|
235
|
+
parts.append("No milestone exists yet for this conversation.")
|
|
236
|
+
parts.append(f"SEED TITLE: {seed_title if seed_title else '(none supplied)'}")
|
|
237
|
+
parts.append(
|
|
238
|
+
"SEED PROMPT: "
|
|
239
|
+
+ (seed_prompt.strip() if seed_prompt and seed_prompt.strip() else "(none supplied)")
|
|
240
|
+
)
|
|
241
|
+
parts.append("")
|
|
242
|
+
parts.append("---")
|
|
243
|
+
parts.append(
|
|
244
|
+
"Discuss the new milestone's goal and scope with the operator — what "
|
|
245
|
+
"it's for, roughly what it covers, any target due date. Once you "
|
|
246
|
+
"both agree on a title (and optionally a description/due date), "
|
|
247
|
+
"present the exact values you'll use, and only after the operator "
|
|
248
|
+
"explicitly confirms, create it with:\n\n"
|
|
249
|
+
f" coord milestone create {repo_name} --title '<title>' "
|
|
250
|
+
"[--description '<desc>'] [--due-on <iso8601>]\n\n"
|
|
251
|
+
"Single-quote the title/description values (never double-quote "
|
|
252
|
+
"them) — single quotes stop the shell from expanding `$(...)`, "
|
|
253
|
+
"backticks, or `$VAR` before `coord` sees the text. If a value "
|
|
254
|
+
"contains a single quote, escape it as `'\\''` (close the quote, "
|
|
255
|
+
"insert an escaped quote, reopen), e.g. \"operator's plan\" becomes "
|
|
256
|
+
"'operator'\\''s plan'.\n\n"
|
|
257
|
+
"Report the printed milestone number back to the operator. There is "
|
|
258
|
+
"no tracking issue yet and none is created here — filing one (and "
|
|
259
|
+
"assigning it to the new milestone with `coord milestone assign`) is "
|
|
260
|
+
"a separate, later step."
|
|
261
|
+
)
|
|
262
|
+
return "\n".join(parts)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclasses.dataclass
|
|
266
|
+
class MilestoneChatBriefing:
|
|
267
|
+
"""Resolved milestone-chat context (#1029) — shared by the headless
|
|
268
|
+
(:func:`dispatch_milestone_chat`) and interactive
|
|
269
|
+
(``coord/commands/dispatch_workers.py::_dispatch_milestone_chat_of``)
|
|
270
|
+
dispatch paths so their seed prompts can never drift apart.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
repo_github: str
|
|
274
|
+
tracking_title: str
|
|
275
|
+
milestone_title: str
|
|
276
|
+
milestone_number: int | None
|
|
277
|
+
briefing: str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def resolve_milestone_chat_briefing(
|
|
281
|
+
repo_name: str,
|
|
282
|
+
tracking_issue_number: int,
|
|
283
|
+
config: Config,
|
|
284
|
+
*,
|
|
285
|
+
add_child_issue: int | None = None,
|
|
286
|
+
) -> MilestoneChatBriefing:
|
|
287
|
+
"""Resolve the milestone/tracking-issue context and build the seed
|
|
288
|
+
briefing for a milestone-chat session (#1029 extraction — this used to
|
|
289
|
+
be the top half of :func:`dispatch_milestone_chat`, inlined).
|
|
290
|
+
|
|
291
|
+
*add_child_issue* (#1017) is an optional candidate child issue number —
|
|
292
|
+
when given, it's fetched and passed to
|
|
293
|
+
:func:`build_milestone_chat_briefing` as ``candidate_child_issue`` so
|
|
294
|
+
the seed steers the conversation toward an "Add sub-issue" chat (the
|
|
295
|
+
TUI's "Add sub-issue via chat…" entry, or `coord milestone chat
|
|
296
|
+
--add-child`). Best-effort: a fetch failure falls back to a plain
|
|
297
|
+
milestone chat rather than failing the whole dispatch.
|
|
298
|
+
|
|
299
|
+
Raises ``RuntimeError`` when the repo is unknown, the tracking issue
|
|
300
|
+
can't be fetched, or it has no milestone (and no ``add_child_issue`` to
|
|
301
|
+
fall back on).
|
|
302
|
+
"""
|
|
303
|
+
repo_cfg = config.repo(repo_name)
|
|
304
|
+
if repo_cfg is None:
|
|
305
|
+
raise RuntimeError(f"repo {repo_name!r} not in coordinator.yml")
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
issue_data = github_ops.get_issue(repo_cfg.github, tracking_issue_number)
|
|
309
|
+
except RuntimeError as e:
|
|
310
|
+
raise RuntimeError(f"could not fetch #{tracking_issue_number}: {e}") from e
|
|
311
|
+
|
|
312
|
+
milestone = issue_data.get("milestone") or {}
|
|
313
|
+
milestone_number = milestone.get("number")
|
|
314
|
+
if milestone_number is None:
|
|
315
|
+
# #1017: the "add sub-issue" chat targets an epic tracking issue's
|
|
316
|
+
# `## Sub-issues` checklist via `coord milestone add-child`, which
|
|
317
|
+
# operates purely on the issue *body* and does NOT require the epic
|
|
318
|
+
# to carry a GitHub milestone. Requiring one here was the root cause
|
|
319
|
+
# of the smoke-test "silent no-op": `coord milestone chat <repo>
|
|
320
|
+
# <epic> --add-child <issue>` bailed with `#<epic> has no milestone`
|
|
321
|
+
# (exit 1) for any epic without a GitHub milestone — the common case.
|
|
322
|
+
# Fall back to the epic's own title as the chat label and skip the
|
|
323
|
+
# milestone-scoped GitHub fetches below; `build_milestone_chat_briefing`
|
|
324
|
+
# already tolerates `milestone_number=None`. A plain milestone chat
|
|
325
|
+
# (no --add-child) still legitimately requires a milestone.
|
|
326
|
+
if add_child_issue is None:
|
|
327
|
+
raise RuntimeError(f"#{tracking_issue_number} has no milestone")
|
|
328
|
+
milestone_title = issue_data.get("title") or f"#{tracking_issue_number}"
|
|
329
|
+
else:
|
|
330
|
+
milestone_title = milestone.get("title") or f"#{milestone_number}"
|
|
331
|
+
|
|
332
|
+
# A milestone-less epic (add-child path, see above) has no milestone to
|
|
333
|
+
# scope issues to and no milestone metadata to fetch — skip both.
|
|
334
|
+
issues = (
|
|
335
|
+
_fetch_milestone_issues(repo_cfg.github, milestone_number)
|
|
336
|
+
if milestone_number is not None
|
|
337
|
+
else []
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Best-effort: pull the milestone's own description/due date so an
|
|
341
|
+
# "edit this milestone" conversation has current values to propose
|
|
342
|
+
# changes against (#1009). Never fatal — the chat still works without it.
|
|
343
|
+
milestone_description: str | None = None
|
|
344
|
+
milestone_due_on: str | None = None
|
|
345
|
+
if milestone_number is not None:
|
|
346
|
+
try:
|
|
347
|
+
ms_data = github_ops.get_milestone(repo_cfg.github, milestone_number)
|
|
348
|
+
milestone_description = ms_data.get("description")
|
|
349
|
+
milestone_due_on = ms_data.get("due_on")
|
|
350
|
+
except RuntimeError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
# #1017: best-effort fetch of the candidate child issue for an "Add
|
|
354
|
+
# sub-issue" chat. A fetch failure just falls back to a plain milestone
|
|
355
|
+
# chat rather than failing the whole dispatch.
|
|
356
|
+
candidate_child_issue: dict | None = None
|
|
357
|
+
if add_child_issue is not None:
|
|
358
|
+
try:
|
|
359
|
+
child_data = github_ops.get_issue(repo_cfg.github, add_child_issue)
|
|
360
|
+
except RuntimeError:
|
|
361
|
+
child_data = None
|
|
362
|
+
if child_data is not None:
|
|
363
|
+
candidate_child_issue = {
|
|
364
|
+
"number": add_child_issue,
|
|
365
|
+
"title": child_data.get("title") or "",
|
|
366
|
+
"body": child_data.get("body") or "",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
briefing = build_milestone_chat_briefing(
|
|
370
|
+
repo_name=repo_name,
|
|
371
|
+
repo_slug=repo_cfg.github,
|
|
372
|
+
milestone_title=milestone_title,
|
|
373
|
+
tracking_issue_number=tracking_issue_number,
|
|
374
|
+
tracking_issue_body=issue_data.get("body") or "",
|
|
375
|
+
issues=issues,
|
|
376
|
+
milestone_number=milestone_number,
|
|
377
|
+
milestone_description=milestone_description,
|
|
378
|
+
milestone_due_on=milestone_due_on,
|
|
379
|
+
candidate_child_issue=candidate_child_issue,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
tracking_title = issue_data.get("title") or f"Milestone chat #{tracking_issue_number}"
|
|
383
|
+
|
|
384
|
+
return MilestoneChatBriefing(
|
|
385
|
+
repo_github=repo_cfg.github,
|
|
386
|
+
tracking_title=tracking_title,
|
|
387
|
+
milestone_title=milestone_title,
|
|
388
|
+
milestone_number=milestone_number,
|
|
389
|
+
briefing=briefing,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def dispatch_milestone_chat(
|
|
394
|
+
repo_name: str,
|
|
395
|
+
tracking_issue_number: int,
|
|
396
|
+
config: Config,
|
|
397
|
+
*,
|
|
398
|
+
machine_override: str | None = None,
|
|
399
|
+
add_child_issue: int | None = None,
|
|
400
|
+
) -> tuple[str, str]:
|
|
401
|
+
"""End-to-end: resolve the milestone, pick a machine, seed the
|
|
402
|
+
briefing, dispatch a ``type="milestone-chat"`` assignment.
|
|
403
|
+
|
|
404
|
+
Returns ``(assignment_id, machine_name)``. Raises ``RuntimeError`` when
|
|
405
|
+
the repo is unknown, the tracking issue can't be fetched or has no
|
|
406
|
+
milestone, no machine claims the repo, or the agent rejects the
|
|
407
|
+
dispatch.
|
|
408
|
+
"""
|
|
409
|
+
repo_cfg = config.repo(repo_name)
|
|
410
|
+
if repo_cfg is None:
|
|
411
|
+
raise RuntimeError(f"repo {repo_name!r} not in coordinator.yml")
|
|
412
|
+
|
|
413
|
+
ctx = resolve_milestone_chat_briefing(
|
|
414
|
+
repo_name,
|
|
415
|
+
tracking_issue_number,
|
|
416
|
+
config,
|
|
417
|
+
add_child_issue=add_child_issue,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Pick the machine.
|
|
421
|
+
if machine_override:
|
|
422
|
+
machine = next(
|
|
423
|
+
(m for m in config.machines if m.name == machine_override),
|
|
424
|
+
None,
|
|
425
|
+
)
|
|
426
|
+
if machine is None:
|
|
427
|
+
raise RuntimeError(f"machine {machine_override!r} not in coordinator.yml")
|
|
428
|
+
if not machine.can_work_on(repo_name):
|
|
429
|
+
raise RuntimeError(
|
|
430
|
+
f"machine {machine_override!r} does not list repo {repo_name!r}"
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
picked = pick_milestone_chat_machine(config, repo_name)
|
|
434
|
+
if picked is None:
|
|
435
|
+
raise RuntimeError(
|
|
436
|
+
f"no machine claims repo {repo_name!r} — milestone-chat needs a "
|
|
437
|
+
"machine that has the repo cloned"
|
|
438
|
+
)
|
|
439
|
+
machine = picked
|
|
440
|
+
|
|
441
|
+
briefing = ctx.briefing
|
|
442
|
+
tracking_title = ctx.tracking_title
|
|
443
|
+
# #1430: deliberately not consulting models.labels — this chat is keyed
|
|
444
|
+
# to the milestone's tracking issue, not a single labelled work issue.
|
|
445
|
+
resolved_model = config.models.default
|
|
446
|
+
proposal = Proposal(
|
|
447
|
+
id=0,
|
|
448
|
+
machine_name=machine.name,
|
|
449
|
+
repo_name=repo_name,
|
|
450
|
+
issue_number=tracking_issue_number,
|
|
451
|
+
issue_title=tracking_title,
|
|
452
|
+
rationale="milestone-chat",
|
|
453
|
+
briefing=briefing,
|
|
454
|
+
model=resolved_model,
|
|
455
|
+
type="milestone-chat",
|
|
456
|
+
required_gates=[],
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
from coord.dispatch import dispatch_with_retry
|
|
460
|
+
from coord.models import Assignment
|
|
461
|
+
from coord.state import record_dispatched_assignment
|
|
462
|
+
|
|
463
|
+
response = dispatch_with_retry(
|
|
464
|
+
proposal,
|
|
465
|
+
config,
|
|
466
|
+
max_retries=config.concurrency.max_retries,
|
|
467
|
+
backoff_base=config.concurrency.backoff_base,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
assignment_id = response.get("id") or uuid.uuid4().hex[:12]
|
|
471
|
+
|
|
472
|
+
asg = Assignment(
|
|
473
|
+
machine_name=machine.name,
|
|
474
|
+
repo_name=repo_name,
|
|
475
|
+
issue_number=tracking_issue_number,
|
|
476
|
+
issue_title=tracking_title,
|
|
477
|
+
files_allowed=[],
|
|
478
|
+
files_forbidden=[],
|
|
479
|
+
briefing=briefing,
|
|
480
|
+
assignment_id=assignment_id,
|
|
481
|
+
status="running",
|
|
482
|
+
dispatched_at=time.time(),
|
|
483
|
+
type="milestone-chat",
|
|
484
|
+
model=resolved_model,
|
|
485
|
+
)
|
|
486
|
+
record_dispatched_assignment(
|
|
487
|
+
assignment=asg,
|
|
488
|
+
repo_github=repo_cfg.github,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return assignment_id, machine.name
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def dispatch_new_milestone_chat(
|
|
495
|
+
repo_name: str,
|
|
496
|
+
config: Config,
|
|
497
|
+
*,
|
|
498
|
+
seed_title: str | None = None,
|
|
499
|
+
seed_prompt: str | None = None,
|
|
500
|
+
machine_override: str | None = None,
|
|
501
|
+
) -> tuple[str, str]:
|
|
502
|
+
"""End-to-end (#1009): pick a machine, seed a brand-new-milestone
|
|
503
|
+
briefing, dispatch a ``type="milestone-chat"`` assignment.
|
|
504
|
+
|
|
505
|
+
Unlike :func:`dispatch_milestone_chat`, there is no existing tracking
|
|
506
|
+
issue or milestone to fetch — this is the "New milestone… → Chat about
|
|
507
|
+
this" entry point, seeded with only the repo and whatever *seed_title*/
|
|
508
|
+
*seed_prompt* the caller supplied. ``issue_number=0`` is used as the
|
|
509
|
+
sentinel for "no real issue yet" — the same established pattern
|
|
510
|
+
``coord/new_issue_chat.py`` and ``coord/refine_chat.py`` (board-level
|
|
511
|
+
chat) already use; the TUI routes ``issue_number == 0`` rows to a
|
|
512
|
+
board-level tab rather than a per-issue one.
|
|
513
|
+
|
|
514
|
+
Returns ``(assignment_id, machine_name)``. Raises ``RuntimeError`` when
|
|
515
|
+
the repo is unknown, no machine claims it, or the agent rejects the
|
|
516
|
+
dispatch.
|
|
517
|
+
"""
|
|
518
|
+
repo_cfg = config.repo(repo_name)
|
|
519
|
+
if repo_cfg is None:
|
|
520
|
+
raise RuntimeError(f"repo {repo_name!r} not in coordinator.yml")
|
|
521
|
+
|
|
522
|
+
if machine_override:
|
|
523
|
+
machine = next(
|
|
524
|
+
(m for m in config.machines if m.name == machine_override),
|
|
525
|
+
None,
|
|
526
|
+
)
|
|
527
|
+
if machine is None:
|
|
528
|
+
raise RuntimeError(f"machine {machine_override!r} not in coordinator.yml")
|
|
529
|
+
if not machine.can_work_on(repo_name):
|
|
530
|
+
raise RuntimeError(
|
|
531
|
+
f"machine {machine_override!r} does not list repo {repo_name!r}"
|
|
532
|
+
)
|
|
533
|
+
else:
|
|
534
|
+
picked = pick_milestone_chat_machine(config, repo_name)
|
|
535
|
+
if picked is None:
|
|
536
|
+
raise RuntimeError(
|
|
537
|
+
f"no machine claims repo {repo_name!r} — milestone-chat needs a "
|
|
538
|
+
"machine that has the repo cloned"
|
|
539
|
+
)
|
|
540
|
+
machine = picked
|
|
541
|
+
|
|
542
|
+
briefing = build_new_milestone_chat_briefing(
|
|
543
|
+
repo_name=repo_name,
|
|
544
|
+
repo_slug=repo_cfg.github,
|
|
545
|
+
seed_title=seed_title,
|
|
546
|
+
seed_prompt=seed_prompt,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
draft_title = seed_title or "(new milestone draft)"
|
|
550
|
+
# #1430: deliberately not consulting models.labels — a brand-new
|
|
551
|
+
# milestone draft has no issue/labels to resolve against yet.
|
|
552
|
+
resolved_model = config.models.default
|
|
553
|
+
proposal = Proposal(
|
|
554
|
+
id=0,
|
|
555
|
+
machine_name=machine.name,
|
|
556
|
+
repo_name=repo_name,
|
|
557
|
+
# No tracking issue exists yet — 0 is the established sentinel
|
|
558
|
+
# (see coord/new_issue_chat.py, coord/refine_chat.py).
|
|
559
|
+
issue_number=0,
|
|
560
|
+
issue_title=draft_title,
|
|
561
|
+
rationale="milestone-chat",
|
|
562
|
+
briefing=briefing,
|
|
563
|
+
model=resolved_model,
|
|
564
|
+
type="milestone-chat",
|
|
565
|
+
required_gates=[],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
from coord.dispatch import dispatch_with_retry
|
|
569
|
+
from coord.models import Assignment
|
|
570
|
+
from coord.state import record_dispatched_assignment
|
|
571
|
+
|
|
572
|
+
response = dispatch_with_retry(
|
|
573
|
+
proposal,
|
|
574
|
+
config,
|
|
575
|
+
max_retries=config.concurrency.max_retries,
|
|
576
|
+
backoff_base=config.concurrency.backoff_base,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
assignment_id = response.get("id") or uuid.uuid4().hex[:12]
|
|
580
|
+
|
|
581
|
+
asg = Assignment(
|
|
582
|
+
machine_name=machine.name,
|
|
583
|
+
repo_name=repo_name,
|
|
584
|
+
issue_number=0,
|
|
585
|
+
issue_title=draft_title,
|
|
586
|
+
files_allowed=[],
|
|
587
|
+
files_forbidden=[],
|
|
588
|
+
briefing=briefing,
|
|
589
|
+
assignment_id=assignment_id,
|
|
590
|
+
status="running",
|
|
591
|
+
dispatched_at=time.time(),
|
|
592
|
+
type="milestone-chat",
|
|
593
|
+
model=resolved_model,
|
|
594
|
+
)
|
|
595
|
+
record_dispatched_assignment(
|
|
596
|
+
assignment=asg,
|
|
597
|
+
repo_github=repo_cfg.github,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return assignment_id, machine.name
|