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/gate_b.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Gate B — post-milestone architecture review (#933, docs/PIPELINE_V2.md).
|
|
2
|
+
|
|
3
|
+
After every issue in a milestone has landed, Gate B dispatches an
|
|
4
|
+
**independent** review that looks at the *assembled* milestone against the
|
|
5
|
+
Gate-A contract (``tests/acceptance/ms-NN/contract.md``, #930) and answers:
|
|
6
|
+
was it implemented to spec? Distinct from the per-issue adversarial code
|
|
7
|
+
review (``coord/review.py``'s ``dispatch_review``, one PR diff at a time) —
|
|
8
|
+
this reviews whether the pieces from separate issues actually integrated and
|
|
9
|
+
whether the assembled result matches what Gate A specified, not whether any
|
|
10
|
+
one diff is individually correct (that already happened at each issue's own
|
|
11
|
+
Review stage).
|
|
12
|
+
|
|
13
|
+
Routed through the **review pipeline**, not ``coord assign`` (docs/
|
|
14
|
+
PIPELINE_V2.md's own wording): the reviewer needs the same independence +
|
|
15
|
+
"coordinator posts to GitHub on your behalf" model as a per-issue review, and
|
|
16
|
+
workers dispatched via ``coord assign`` have ``gh`` denied. Concretely this
|
|
17
|
+
means reusing ``type="review"`` + ``coord.review.REVIEWER_SYSTEM_PROMPT``
|
|
18
|
+
verbatim rather than inventing a new assignment type — which also means
|
|
19
|
+
``coord.notify``'s existing ``REVIEW_VERDICT``/``REVIEW_BODY`` parsing and
|
|
20
|
+
posting need zero changes: :func:`dispatch_gate_b_review` sets
|
|
21
|
+
``review_target`` to a non-numeric sentinel (there is no PR to post to, only
|
|
22
|
+
the tracking issue), and ``coord.notify._try_parse_and_post_review`` already
|
|
23
|
+
falls back to posting a plain issue comment whenever ``int(review_target)``
|
|
24
|
+
fails.
|
|
25
|
+
|
|
26
|
+
Like ``coord milestone gate-c`` (#932), dispatching Gate B is an explicit
|
|
27
|
+
manual operator action, and a request-changes verdict is surfaced (as a
|
|
28
|
+
tracking-issue comment) rather than driving anything automatically. Its
|
|
29
|
+
verdict IS consulted, read-only, by ``coord milestone ship`` (#934, Gate D,
|
|
30
|
+
``coord/commands/milestone.py``) via :func:`latest_gate_b_verdict` — ship
|
|
31
|
+
refuses ``feature/ms-NN -> develop`` until this review is ``"approve"``, but
|
|
32
|
+
shipping itself is still a separate, explicit operator action.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import time
|
|
38
|
+
import uuid
|
|
39
|
+
from typing import TYPE_CHECKING, Callable
|
|
40
|
+
|
|
41
|
+
import httpx
|
|
42
|
+
|
|
43
|
+
from coord import github_ops
|
|
44
|
+
from coord.acceptance import gate_a_contract_path
|
|
45
|
+
from coord.dispatch import AGENT_PORT
|
|
46
|
+
from coord.models import Assignment, Board, Machine, Repo
|
|
47
|
+
from coord.review import REVIEWER_SYSTEM_PROMPT
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from coord.config import Config
|
|
51
|
+
from coord.milestone_order import WorkOrder
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"GateBError",
|
|
55
|
+
"ContractFetch",
|
|
56
|
+
"IssueFetch",
|
|
57
|
+
"review_target_for",
|
|
58
|
+
"build_gate_b_briefing",
|
|
59
|
+
"dispatch_gate_b_review",
|
|
60
|
+
"latest_gate_b_verdict",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GateBError(Exception):
|
|
65
|
+
"""Gate B's review could not be dispatched (bad machine, network failure)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# (repo_github, path, branch) -> file content, or None if missing. Mirrors
|
|
69
|
+
# coord.milestone_dispatch.ManifestFetch — injected so tests never hit `gh`.
|
|
70
|
+
ContractFetch = Callable[[str, str, str], "str | None"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _default_fetch_repo_file(repo_github: str, path: str, branch: str) -> str | None:
|
|
74
|
+
try:
|
|
75
|
+
return github_ops.get_repo_file(repo_github, path, branch=branch)
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# (repo_github, issue_number) -> the `gh issue view --json` dict. Injected so
|
|
81
|
+
# tests never hit `gh`.
|
|
82
|
+
IssueFetch = Callable[[str, int], dict]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _default_get_issue(repo_github: str, issue_number: int) -> dict:
|
|
86
|
+
return github_ops.get_issue(repo_github, issue_number)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def review_target_for(milestone_number: int) -> str:
|
|
90
|
+
"""The non-numeric ``review_target`` sentinel for a Gate B review.
|
|
91
|
+
|
|
92
|
+
Deliberately not an ``int``-parseable string: there is no PR for a
|
|
93
|
+
milestone-level review to post to, only the tracking issue. Keeping this
|
|
94
|
+
non-numeric is what makes ``coord.notify._try_parse_and_post_review``'s
|
|
95
|
+
existing ``int(review_target)`` sniff fall through to its already-built
|
|
96
|
+
"post as an issue comment" path with no code changes there.
|
|
97
|
+
"""
|
|
98
|
+
return f"gate-b-ms-{milestone_number}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_gate_b_briefing(
|
|
102
|
+
*,
|
|
103
|
+
tracking_issue: int,
|
|
104
|
+
milestone_number: int,
|
|
105
|
+
tracking_issue_title: str,
|
|
106
|
+
tracking_issue_body: str,
|
|
107
|
+
member_issues: list[tuple[int, str]],
|
|
108
|
+
contract_text: str | None,
|
|
109
|
+
repo_github: str,
|
|
110
|
+
default_branch: str,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Assemble the Gate B reviewer's prompt. Pure function — easy to test.
|
|
113
|
+
|
|
114
|
+
Unlike ``coord.review.build_review_briefing`` (one issue's PR diff), there
|
|
115
|
+
is no single diff here: the reviewer checks out *default_branch* and
|
|
116
|
+
looks at the assembled state of every issue in *member_issues* against
|
|
117
|
+
*contract_text* (the Gate-A rubric, #930's ``contract.md`` — ``None``
|
|
118
|
+
when the milestone has no oracle-loop contract, falling back to the
|
|
119
|
+
tracking issue body as the rubric).
|
|
120
|
+
"""
|
|
121
|
+
lines: list[str] = []
|
|
122
|
+
lines.append(f"# Gate B review: milestone ms-{milestone_number} ({repo_github})")
|
|
123
|
+
lines.append("")
|
|
124
|
+
lines.append(
|
|
125
|
+
"You are an independent architecture reviewer dispatched by the "
|
|
126
|
+
"coordinator for a **post-milestone** review (Gate B, "
|
|
127
|
+
"docs/PIPELINE_V2.md) — distinct from a per-issue code review. Every "
|
|
128
|
+
f"issue below already landed on `{default_branch}` individually and "
|
|
129
|
+
"passed its own Work -> Test -> Review -> Merge pipeline. Your job is "
|
|
130
|
+
"different: look at the **assembled milestone as a whole** and "
|
|
131
|
+
"answer **was it implemented to spec?**"
|
|
132
|
+
)
|
|
133
|
+
lines.append("")
|
|
134
|
+
|
|
135
|
+
lines.append("## Tracking issue")
|
|
136
|
+
lines.append(f"**#{tracking_issue}: {tracking_issue_title}**")
|
|
137
|
+
if tracking_issue_body.strip():
|
|
138
|
+
lines.append("")
|
|
139
|
+
lines.append(tracking_issue_body.strip())
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
lines.append("## Issues in this milestone")
|
|
143
|
+
lines.append("")
|
|
144
|
+
for num, title in member_issues:
|
|
145
|
+
lines.append(f"- #{num}: {title}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
if contract_text and contract_text.strip():
|
|
149
|
+
lines.append("## Gate-A contract (the rubric)")
|
|
150
|
+
lines.append("")
|
|
151
|
+
lines.append(
|
|
152
|
+
"This is the black-box contract authored at Gate A, *before* any "
|
|
153
|
+
"of the above issues were implemented — check the assembled "
|
|
154
|
+
"result against it."
|
|
155
|
+
)
|
|
156
|
+
lines.append("")
|
|
157
|
+
lines.append("```markdown")
|
|
158
|
+
lines.append(contract_text.strip())
|
|
159
|
+
lines.append("```")
|
|
160
|
+
lines.append("")
|
|
161
|
+
else:
|
|
162
|
+
lines.append("## No Gate-A contract found")
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append(
|
|
165
|
+
"No `contract.md` was found for this milestone — fall back to "
|
|
166
|
+
"the tracking issue body above (and any design docs it links) as "
|
|
167
|
+
"the rubric."
|
|
168
|
+
)
|
|
169
|
+
lines.append("")
|
|
170
|
+
|
|
171
|
+
lines.append("## What to do")
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.append(
|
|
174
|
+
f"1. `git fetch origin && git checkout origin/{default_branch}` — you "
|
|
175
|
+
"are reviewing the current assembled state, not a single PR diff."
|
|
176
|
+
)
|
|
177
|
+
lines.append(
|
|
178
|
+
"2. Read the code that implements each issue above (`git log "
|
|
179
|
+
"--oneline --grep '#<N>'`, or search for the files each issue "
|
|
180
|
+
"touched)."
|
|
181
|
+
)
|
|
182
|
+
lines.append(
|
|
183
|
+
"3. Compare the assembled result against the Gate-A contract (and "
|
|
184
|
+
"the tracking issue's design). Specifically look for:"
|
|
185
|
+
)
|
|
186
|
+
lines.append(
|
|
187
|
+
" - Does the implementation match the contract's black-box "
|
|
188
|
+
"surface (CLI names, screen text, API field shapes)?"
|
|
189
|
+
)
|
|
190
|
+
lines.append(
|
|
191
|
+
" - Did the pieces from separate issues actually integrate, or "
|
|
192
|
+
"are there gaps/seams between them?"
|
|
193
|
+
)
|
|
194
|
+
lines.append(
|
|
195
|
+
" - Anything the contract promised that never landed, or landed "
|
|
196
|
+
"differently than specified?"
|
|
197
|
+
)
|
|
198
|
+
lines.append(
|
|
199
|
+
"4. You are NOT re-litigating code style or decisions already "
|
|
200
|
+
"approved in each issue's own review — this is a **spec adherence "
|
|
201
|
+
"and integration** check, not a second code review."
|
|
202
|
+
)
|
|
203
|
+
lines.append(
|
|
204
|
+
"5. Do NOT touch README/CHANGELOG or any other docs, and do not "
|
|
205
|
+
"modify any code — you are read-only, same as a per-issue reviewer."
|
|
206
|
+
)
|
|
207
|
+
lines.append("")
|
|
208
|
+
lines.append(
|
|
209
|
+
"6. At the END of your session, output your verdict in this exact "
|
|
210
|
+
"format (the coordinator posts it to the tracking issue on your "
|
|
211
|
+
"behalf — do NOT run any `gh` commands):"
|
|
212
|
+
)
|
|
213
|
+
lines.append("")
|
|
214
|
+
lines.append("```")
|
|
215
|
+
lines.append("REVIEW_VERDICT: approve")
|
|
216
|
+
lines.append("REVIEW_BODY:")
|
|
217
|
+
lines.append("<your full built-to-spec assessment in markdown>")
|
|
218
|
+
lines.append("END_REVIEW")
|
|
219
|
+
lines.append("```")
|
|
220
|
+
lines.append("")
|
|
221
|
+
lines.append(
|
|
222
|
+
"Use `REVIEW_VERDICT: request-changes` if the assembled milestone "
|
|
223
|
+
"diverges from the Gate-A contract or has integration gaps between "
|
|
224
|
+
"issues — this is the signal that **bounces** the milestone back "
|
|
225
|
+
"for fixes rather than shipping (docs/PIPELINE_V2.md's \"a gate can "
|
|
226
|
+
"bounce backwards\" principle)."
|
|
227
|
+
)
|
|
228
|
+
lines.append(
|
|
229
|
+
"`END_REVIEW` is a HARD REQUIREMENT (#1427): an otherwise-complete "
|
|
230
|
+
"verdict with no `END_REVIEW` line is discarded in its entirety, "
|
|
231
|
+
"not recorded with a best guess. Write `END_REVIEW` as the literal "
|
|
232
|
+
"last line of your final message even if your assessment already "
|
|
233
|
+
"feels finished."
|
|
234
|
+
)
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def dispatch_gate_b_review(
|
|
239
|
+
*,
|
|
240
|
+
repo_cfg: Repo,
|
|
241
|
+
config: "Config",
|
|
242
|
+
machine: Machine,
|
|
243
|
+
tracking_issue: int,
|
|
244
|
+
milestone_number: int,
|
|
245
|
+
work_order: "WorkOrder",
|
|
246
|
+
board: Board,
|
|
247
|
+
http_client: httpx.Client | None = None,
|
|
248
|
+
contract_fetch: ContractFetch | None = None,
|
|
249
|
+
issue_fetch: IssueFetch | None = None,
|
|
250
|
+
now: float | None = None,
|
|
251
|
+
) -> Assignment:
|
|
252
|
+
"""Dispatch a Gate B review (#933) to *machine* and record it.
|
|
253
|
+
|
|
254
|
+
Mirrors ``coord.review.dispatch_review``'s raw-POST-to-``/assign`` shape
|
|
255
|
+
(a plain dict payload, not a :class:`~coord.models.Proposal` — a review
|
|
256
|
+
needs ``system_prompt``/``review_target`` fields the dataclass doesn't
|
|
257
|
+
carry) but is standalone rather than keyed off one completed work
|
|
258
|
+
assignment's PR. Reuses ``type="review"`` + ``REVIEWER_SYSTEM_PROMPT``
|
|
259
|
+
verbatim so ``coord.notify``'s existing verdict parsing/posting needs no
|
|
260
|
+
changes — see :func:`review_target_for` for how the no-PR case falls
|
|
261
|
+
through to an issue-comment post automatically.
|
|
262
|
+
|
|
263
|
+
Raises :class:`GateBError` if *machine* has no ``repo_path`` for
|
|
264
|
+
*repo_cfg*, or rejects/can't reach the dispatch. Caller is responsible
|
|
265
|
+
for persisting the board (mirrors ``coord.milestone_dispatch.
|
|
266
|
+
dispatch_entry``'s contract: this appends the new running Assignment to
|
|
267
|
+
*board*'s ``active`` list in place, but ``record_dispatched_assignment``
|
|
268
|
+
already wrote the durable row).
|
|
269
|
+
"""
|
|
270
|
+
fetch_contract = contract_fetch or _default_fetch_repo_file
|
|
271
|
+
fetch_issue = issue_fetch or _default_get_issue
|
|
272
|
+
|
|
273
|
+
contract_text = fetch_contract(
|
|
274
|
+
repo_cfg.github, gate_a_contract_path(milestone_number), repo_cfg.default_branch,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
tracking = fetch_issue(repo_cfg.github, tracking_issue)
|
|
278
|
+
tracking_title = tracking.get("title") or f"Issue #{tracking_issue}"
|
|
279
|
+
tracking_body = tracking.get("body") or ""
|
|
280
|
+
|
|
281
|
+
member_issues: list[tuple[int, str]] = []
|
|
282
|
+
for node in work_order.nodes:
|
|
283
|
+
try:
|
|
284
|
+
data = fetch_issue(repo_cfg.github, node.issue_number)
|
|
285
|
+
title = data.get("title") or f"Issue #{node.issue_number}"
|
|
286
|
+
except RuntimeError:
|
|
287
|
+
title = "(title unavailable)"
|
|
288
|
+
member_issues.append((node.issue_number, title))
|
|
289
|
+
|
|
290
|
+
briefing = build_gate_b_briefing(
|
|
291
|
+
tracking_issue=tracking_issue,
|
|
292
|
+
milestone_number=milestone_number,
|
|
293
|
+
tracking_issue_title=tracking_title,
|
|
294
|
+
tracking_issue_body=tracking_body,
|
|
295
|
+
member_issues=member_issues,
|
|
296
|
+
contract_text=contract_text,
|
|
297
|
+
repo_github=repo_cfg.github,
|
|
298
|
+
default_branch=repo_cfg.default_branch,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
repo_path = machine.repo_path(repo_cfg.name)
|
|
302
|
+
if repo_path is None:
|
|
303
|
+
raise GateBError(
|
|
304
|
+
f"machine {machine.name!r} has no repo_path configured for {repo_cfg.name!r}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
review_target = review_target_for(milestone_number)
|
|
308
|
+
# #1430: deliberately not consulting models.labels — Gate B reviews the
|
|
309
|
+
# *assembled milestone* against the tracking issue, not a single labelled
|
|
310
|
+
# work issue, so there's no single tier label to resolve against.
|
|
311
|
+
payload = {
|
|
312
|
+
"repo_name": repo_cfg.name,
|
|
313
|
+
"repo_path": repo_path,
|
|
314
|
+
"issue_number": tracking_issue,
|
|
315
|
+
"issue_title": f"[gate-b] {tracking_title}",
|
|
316
|
+
"briefing": briefing,
|
|
317
|
+
"files_allowed": [],
|
|
318
|
+
"files_forbidden": [],
|
|
319
|
+
"pull_repos": [],
|
|
320
|
+
"type": "review",
|
|
321
|
+
"model": config.models.default,
|
|
322
|
+
"system_prompt": REVIEWER_SYSTEM_PROMPT,
|
|
323
|
+
"review_target": review_target,
|
|
324
|
+
"branch": repo_cfg.default_branch or "main",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
client = http_client or httpx
|
|
328
|
+
url = f"http://{machine.host}:{AGENT_PORT}/assign"
|
|
329
|
+
try:
|
|
330
|
+
resp = client.post(url, json=payload, timeout=15)
|
|
331
|
+
resp.raise_for_status()
|
|
332
|
+
agent_response = resp.json()
|
|
333
|
+
except (httpx.HTTPError, httpx.TimeoutException) as exc:
|
|
334
|
+
raise GateBError(
|
|
335
|
+
f"agent {machine.name!r} rejected the Gate B dispatch: {exc}"
|
|
336
|
+
) from exc
|
|
337
|
+
|
|
338
|
+
assignment = Assignment(
|
|
339
|
+
machine_name=machine.name,
|
|
340
|
+
repo_name=repo_cfg.name,
|
|
341
|
+
issue_number=tracking_issue,
|
|
342
|
+
issue_title=f"[gate-b] {tracking_title}",
|
|
343
|
+
files_allowed=[],
|
|
344
|
+
files_forbidden=[],
|
|
345
|
+
briefing=briefing,
|
|
346
|
+
assignment_id=agent_response.get("id") or uuid.uuid4().hex[:12],
|
|
347
|
+
status="running",
|
|
348
|
+
branch=repo_cfg.default_branch,
|
|
349
|
+
dispatched_at=now if now is not None else time.time(),
|
|
350
|
+
type="review",
|
|
351
|
+
review_target=review_target,
|
|
352
|
+
model=config.models.default,
|
|
353
|
+
)
|
|
354
|
+
board.active.append(assignment)
|
|
355
|
+
|
|
356
|
+
from coord.state import record_dispatched_assignment # noqa: PLC0415
|
|
357
|
+
|
|
358
|
+
record_dispatched_assignment(assignment=assignment, repo_github=repo_cfg.github)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
github_ops.post_issue_comment(
|
|
362
|
+
repo_cfg.github,
|
|
363
|
+
tracking_issue,
|
|
364
|
+
"## Gate B: post-milestone architecture review dispatched\n\n"
|
|
365
|
+
f"Reviewer machine: `{machine.name}` \n"
|
|
366
|
+
f"Assignment: `{assignment.assignment_id}`\n\n"
|
|
367
|
+
"This is an independent review of the *assembled* milestone "
|
|
368
|
+
"against its Gate-A contract (docs/PIPELINE_V2.md) — the "
|
|
369
|
+
"verdict will be posted here as a comment when it completes.",
|
|
370
|
+
)
|
|
371
|
+
except RuntimeError:
|
|
372
|
+
# Best-effort — mirrors dispatch_entry's post_briefing posture: a
|
|
373
|
+
# failed announcement comment must never abort an already-accepted
|
|
374
|
+
# dispatch.
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
return assignment
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def latest_gate_b_verdict(
|
|
381
|
+
board: Board, repo_name: str, tracking_issue: int, milestone_number: int,
|
|
382
|
+
) -> str | None:
|
|
383
|
+
"""The most recent Gate B verdict for this milestone, or ``None`` if no
|
|
384
|
+
Gate B review has completed yet.
|
|
385
|
+
|
|
386
|
+
Scans both ``board.completed`` and ``board.active`` (mirrors ``coord.
|
|
387
|
+
merge_queue.has_approved_review``'s posture: a verdict just posted by
|
|
388
|
+
``coord.notify`` may still be on ``active`` for a tick before reconcile
|
|
389
|
+
moves it) for the ``type="review"`` assignment dispatched by
|
|
390
|
+
:func:`dispatch_gate_b_review` — identified by ``issue_number ==
|
|
391
|
+
tracking_issue`` and ``review_target == review_target_for(milestone_
|
|
392
|
+
number)`` (the ``gate-b-ms-N`` sentinel, unambiguous across milestones
|
|
393
|
+
and repos on the same board). When more than one exists (a re-dispatched
|
|
394
|
+
Gate B after `request-changes`), the most recently dispatched one wins.
|
|
395
|
+
|
|
396
|
+
Used by ``coord milestone ship`` (#934, Gate D) to refuse shipping
|
|
397
|
+
``feature/ms-NN -> develop`` until Gate B is ``"approve"``.
|
|
398
|
+
"""
|
|
399
|
+
target = review_target_for(milestone_number)
|
|
400
|
+
pool = list(getattr(board, "completed", []) or []) + list(getattr(board, "active", []) or [])
|
|
401
|
+
candidates = [
|
|
402
|
+
a for a in pool
|
|
403
|
+
if getattr(a, "type", None) == "review"
|
|
404
|
+
and getattr(a, "repo_name", None) == repo_name
|
|
405
|
+
and getattr(a, "issue_number", None) == tracking_issue
|
|
406
|
+
and getattr(a, "review_target", None) == target
|
|
407
|
+
]
|
|
408
|
+
if not candidates:
|
|
409
|
+
return None
|
|
410
|
+
candidates.sort(key=lambda a: getattr(a, "dispatched_at", 0) or 0)
|
|
411
|
+
return candidates[-1].review_verdict
|