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/commands/drive.py
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""``coord drive`` — the unattended single-issue driver (#1392).
|
|
2
|
+
|
|
3
|
+
Thin Click wrapper: parse flags into :class:`~coord.drive.DriveOptions`, build
|
|
4
|
+
the :class:`~coord.drive.Driver`, translate :class:`~coord.drive.DriveError`
|
|
5
|
+
into a clean exit code. Every decision lives in ``coord/drive.py``.
|
|
6
|
+
|
|
7
|
+
Replaces ``scripts/drive-issue.sh`` + ``scripts/coord_issue_state.py``, deleted
|
|
8
|
+
in the same change (no two implementations). ``scripts/coord-test-runner.sh``
|
|
9
|
+
deliberately stays as shell — it is genuinely subprocess/venv/cargo-env work,
|
|
10
|
+
and its four sharp-edged parsers already live in ``coord/test_report.py``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from coord.commands._common import _CONFIG_OPTION, _load_config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_DRIVE_HELP = """Drive ONE issue Work → Test → Review → Merge, unattended.
|
|
26
|
+
|
|
27
|
+
A resumable state machine over the daemon's board: it dispatches the WORK
|
|
28
|
+
assignment, then OBSERVES Test/Review/Merge (coord dispatches all three
|
|
29
|
+
itself), looping a failing test through `coord fix` on the same branch.
|
|
30
|
+
Re-running it on the same issue is safe and resumes from wherever the board
|
|
31
|
+
actually is.
|
|
32
|
+
|
|
33
|
+
#1453: when this issue resolves to a milestone with a merged Gate-A contract
|
|
34
|
+
(`tests/acceptance/ms-NN/contract.md`) and the repo has an acceptance driver
|
|
35
|
+
configured, the sealed JIT acceptance slice is authored first (`coord
|
|
36
|
+
acceptance author <repo> <tracking_issue> --issue <N>`) and observed through
|
|
37
|
+
to a landed merge — ONLY THEN is `coord assign`/`coord approve-plan` run.
|
|
38
|
+
Pass --no-acceptance to skip this and drive the issue exactly as before.
|
|
39
|
+
The preflight banner's "acceptance" line always states which mode a run is
|
|
40
|
+
in and why.
|
|
41
|
+
|
|
42
|
+
Exit codes: 0 merged (verified against the remote default branch), or review
|
|
43
|
+
approved with --no-merge; 1 a stage reached a terminal failure a script cannot
|
|
44
|
+
resolve; 2 bad usage / configuration; 3 deadline exceeded.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _rebuild_drive_argv(
|
|
49
|
+
repo: str,
|
|
50
|
+
issue: int,
|
|
51
|
+
*,
|
|
52
|
+
machine: str,
|
|
53
|
+
model: str,
|
|
54
|
+
briefing_file: str,
|
|
55
|
+
do_plan: bool,
|
|
56
|
+
max_fix_rounds: int,
|
|
57
|
+
skip_test: bool,
|
|
58
|
+
repo_path: str,
|
|
59
|
+
poll: float,
|
|
60
|
+
max_work_retries: int,
|
|
61
|
+
deadline_mins: float,
|
|
62
|
+
stall_mins: float,
|
|
63
|
+
notify: bool,
|
|
64
|
+
accept_advisory: bool,
|
|
65
|
+
force_review: bool,
|
|
66
|
+
no_merge: bool,
|
|
67
|
+
merge_method: str,
|
|
68
|
+
max_merge_attempts: int,
|
|
69
|
+
dry_run: bool,
|
|
70
|
+
config_path: Path,
|
|
71
|
+
) -> list[str]:
|
|
72
|
+
"""Rebuild a ``drive <repo> <issue> ...`` argv from parsed flags (`--tmux`).
|
|
73
|
+
|
|
74
|
+
The `--tmux` launch re-execs `coord drive` (minus `--tmux`) detached
|
|
75
|
+
inside a fresh tmux session, so every OTHER flag the operator passed
|
|
76
|
+
must survive the trip. Deliberately always emits every option (rather
|
|
77
|
+
than only the ones that differ from Click's own default) so this stays
|
|
78
|
+
correct even if a default ever changes — no drift between two places
|
|
79
|
+
that both need to know it.
|
|
80
|
+
"""
|
|
81
|
+
argv = ["drive", repo, str(issue)]
|
|
82
|
+
if machine:
|
|
83
|
+
argv += ["--machine", machine]
|
|
84
|
+
if model:
|
|
85
|
+
argv += ["--model", model]
|
|
86
|
+
if briefing_file:
|
|
87
|
+
argv += ["--briefing-file", briefing_file]
|
|
88
|
+
if do_plan:
|
|
89
|
+
argv.append("--plan")
|
|
90
|
+
argv += ["--max-fix-rounds", str(max_fix_rounds)]
|
|
91
|
+
if skip_test:
|
|
92
|
+
argv.append("--skip-test")
|
|
93
|
+
if repo_path:
|
|
94
|
+
argv += ["--repo-path", repo_path]
|
|
95
|
+
argv += ["--poll", str(poll)]
|
|
96
|
+
argv += ["--max-work-retries", str(max_work_retries)]
|
|
97
|
+
argv += ["--deadline", str(deadline_mins)]
|
|
98
|
+
argv += ["--stall", str(stall_mins)]
|
|
99
|
+
if notify:
|
|
100
|
+
argv.append("--notify")
|
|
101
|
+
if accept_advisory:
|
|
102
|
+
argv.append("--accept-advisory")
|
|
103
|
+
if force_review:
|
|
104
|
+
argv.append("--force-review")
|
|
105
|
+
if no_merge:
|
|
106
|
+
argv.append("--no-merge")
|
|
107
|
+
argv += ["--merge-method", merge_method]
|
|
108
|
+
argv += ["--max-merge-attempts", str(max_merge_attempts)]
|
|
109
|
+
if dry_run:
|
|
110
|
+
argv.append("--dry-run")
|
|
111
|
+
if config_path:
|
|
112
|
+
argv += ["--config", str(config_path)]
|
|
113
|
+
return argv
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@click.command("drive", help=_DRIVE_HELP)
|
|
117
|
+
@click.argument("repo")
|
|
118
|
+
@click.argument("issue", type=int)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--machine",
|
|
121
|
+
default="",
|
|
122
|
+
help=(
|
|
123
|
+
"Machine for the work dispatch (default: least-loaded unpaused machine "
|
|
124
|
+
"that hosts the repo)."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
@click.option(
|
|
128
|
+
"--model",
|
|
129
|
+
default="",
|
|
130
|
+
help="Model tier (haiku|sonnet|opus). Default: models.default.",
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--briefing-file",
|
|
134
|
+
default="",
|
|
135
|
+
type=click.Path(),
|
|
136
|
+
help=(
|
|
137
|
+
"REPLACES the entire auto-generated briefing for the work dispatch — it "
|
|
138
|
+
"is NOT an addendum. The issue body, project rules and file scope are "
|
|
139
|
+
"all dropped, so the worker only ever sees this file. To ADD guidance "
|
|
140
|
+
"while keeping the real briefing, use `coord context add --pin <repo> "
|
|
141
|
+
"<issue> '<note>'`, which prepends to every briefing (#603)."
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"--plan",
|
|
146
|
+
"do_plan",
|
|
147
|
+
is_flag=True,
|
|
148
|
+
help=(
|
|
149
|
+
"Run a read-only plan stage first and auto-approve it "
|
|
150
|
+
"(coord assign --plan-only → coord approve-plan)."
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
@click.option(
|
|
154
|
+
"--max-fix-rounds",
|
|
155
|
+
default=3,
|
|
156
|
+
show_default=True,
|
|
157
|
+
help=(
|
|
158
|
+
"Headless `coord fix` rounds this run is willing to spend, shared by "
|
|
159
|
+
"BOTH fix arms: a failing test suite and a request-changes review "
|
|
160
|
+
"(#1692). Each round continues the SAME branch with the model "
|
|
161
|
+
"escalated (sonnet → opus). coord dispatches the Test stage itself "
|
|
162
|
+
"onto a capability-matched machine (#1426); this only observes the "
|
|
163
|
+
"verdict. Distinct from pipeline.max_review_iterations, which bounds "
|
|
164
|
+
"the review loop per ISSUE across every drive."
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
@click.option(
|
|
168
|
+
"--skip-test",
|
|
169
|
+
is_flag=True,
|
|
170
|
+
help=(
|
|
171
|
+
"Record the Test gate as `skipped` directly (via `coord test --skipped`) "
|
|
172
|
+
"— no dispatch. Use only for genuinely untestable diffs."
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
@click.option(
|
|
176
|
+
"--repo-path",
|
|
177
|
+
default="",
|
|
178
|
+
type=click.Path(),
|
|
179
|
+
help=(
|
|
180
|
+
"Local checkout used for branch/merge verification (git fetch + "
|
|
181
|
+
"cherry against origin). Default: ~/src/<repo>."
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
@click.option("--poll", default=60.0, show_default=True, help="Board poll interval (s).")
|
|
185
|
+
@click.option(
|
|
186
|
+
"--max-work-retries",
|
|
187
|
+
default=1,
|
|
188
|
+
show_default=True,
|
|
189
|
+
help="`coord retry` attempts on a failed work stage.",
|
|
190
|
+
)
|
|
191
|
+
@click.option(
|
|
192
|
+
"--deadline",
|
|
193
|
+
"deadline_mins",
|
|
194
|
+
default=240.0,
|
|
195
|
+
show_default=True,
|
|
196
|
+
help="Give up after this long (minutes).",
|
|
197
|
+
)
|
|
198
|
+
@click.option(
|
|
199
|
+
"--stall",
|
|
200
|
+
"stall_mins",
|
|
201
|
+
default=20.0,
|
|
202
|
+
show_default=True,
|
|
203
|
+
help=(
|
|
204
|
+
"Warn (and nudge, with --notify) after this long with no state "
|
|
205
|
+
"change, repeating on the same cadence for as long as the stall "
|
|
206
|
+
"continues."
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"--notify",
|
|
211
|
+
is_flag=True,
|
|
212
|
+
help=(
|
|
213
|
+
"Also run `coord notify` under ~/.coord/notify.lock when stalled, to cut "
|
|
214
|
+
"the 5-minute timer latency. OFF by default: two drivers racing to "
|
|
215
|
+
"dispatch the same thing is the #476/#477 duplicate-fix-worker incident."
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
@click.option(
|
|
219
|
+
"--accept-advisory",
|
|
220
|
+
is_flag=True,
|
|
221
|
+
help=(
|
|
222
|
+
"Proceed when the work row is ADVISORY but its branch demonstrably "
|
|
223
|
+
"carries commits. Needed while #1357 is open."
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--force-review",
|
|
228
|
+
is_flag=True,
|
|
229
|
+
help=(
|
|
230
|
+
"Explicitly request the review for work completed in an INTERACTIVE "
|
|
231
|
+
"session. coord's #555 guard never auto-dispatches one for "
|
|
232
|
+
"provider=claude-pty, so without this the run stops at preflight "
|
|
233
|
+
"rather than stalling forever."
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
@click.option("--no-merge", is_flag=True, help="Stop after the review approves.")
|
|
237
|
+
@click.option(
|
|
238
|
+
"--merge-method",
|
|
239
|
+
type=click.Choice(["rebase", "squash", "merge"]),
|
|
240
|
+
default="rebase",
|
|
241
|
+
show_default=True,
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--max-merge-attempts",
|
|
245
|
+
default=3,
|
|
246
|
+
show_default=True,
|
|
247
|
+
help="`coord merge` attempts before giving up (bounds a merge the board never reflects).",
|
|
248
|
+
)
|
|
249
|
+
@click.option(
|
|
250
|
+
"--dry-run",
|
|
251
|
+
is_flag=True,
|
|
252
|
+
help="Print the resolved plan and current state, then exit.",
|
|
253
|
+
)
|
|
254
|
+
@click.option(
|
|
255
|
+
"--tmux",
|
|
256
|
+
"use_tmux",
|
|
257
|
+
is_flag=True,
|
|
258
|
+
help=(
|
|
259
|
+
"Launch the drive loop DETACHED in a `coord-drive-<repo>-<issue>` tmux "
|
|
260
|
+
"session instead of running inline (#1398). Waits a few seconds to "
|
|
261
|
+
"confirm the session is still alive and has started writing its run "
|
|
262
|
+
"log (#1606) before exiting 0 — a launch that dies immediately (e.g. "
|
|
263
|
+
"nothing left to do) exits non-zero with the reason instead of "
|
|
264
|
+
"printing a false success banner. Once confirmed live, this "
|
|
265
|
+
"invocation exits immediately — the run survives this terminal "
|
|
266
|
+
"closing, a TUI restart, or an ssh drop. Reattach with `coord "
|
|
267
|
+
"drive-attach <repo> <issue>`; list live runs with `coord "
|
|
268
|
+
"drive-sessions`; stop with `coord drive-stop <repo> <issue>` — killing "
|
|
269
|
+
"the session releases the per-issue flock, which IS the correct Stop."
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
@click.option(
|
|
273
|
+
"--no-acceptance",
|
|
274
|
+
is_flag=True,
|
|
275
|
+
help=(
|
|
276
|
+
"Skip the #1453 oracle-loop JIT slice authoring step even when this "
|
|
277
|
+
"issue's milestone has a merged Gate-A contract — use when the "
|
|
278
|
+
"contract is stale/wrong for this issue, or you just want a plain "
|
|
279
|
+
"run. Dispatches straight to `coord assign` as before #1453."
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
@_CONFIG_OPTION
|
|
283
|
+
def drive(
|
|
284
|
+
repo: str,
|
|
285
|
+
issue: int,
|
|
286
|
+
machine: str,
|
|
287
|
+
model: str,
|
|
288
|
+
briefing_file: str,
|
|
289
|
+
do_plan: bool,
|
|
290
|
+
max_fix_rounds: int,
|
|
291
|
+
skip_test: bool,
|
|
292
|
+
repo_path: str,
|
|
293
|
+
poll: float,
|
|
294
|
+
max_work_retries: int,
|
|
295
|
+
deadline_mins: float,
|
|
296
|
+
stall_mins: float,
|
|
297
|
+
notify: bool,
|
|
298
|
+
accept_advisory: bool,
|
|
299
|
+
force_review: bool,
|
|
300
|
+
no_merge: bool,
|
|
301
|
+
merge_method: str,
|
|
302
|
+
max_merge_attempts: int,
|
|
303
|
+
dry_run: bool,
|
|
304
|
+
use_tmux: bool,
|
|
305
|
+
no_acceptance: bool,
|
|
306
|
+
config_path: Path,
|
|
307
|
+
) -> None:
|
|
308
|
+
# Imported lazily so `coord --help` doesn't pay for httpx/subprocess setup.
|
|
309
|
+
from coord.drive import ( # noqa: PLC0415
|
|
310
|
+
Driver,
|
|
311
|
+
DriveError,
|
|
312
|
+
DriveOptions,
|
|
313
|
+
coord_argv,
|
|
314
|
+
launch_drive_in_tmux,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if use_tmux:
|
|
318
|
+
argv = coord_argv() + _rebuild_drive_argv(
|
|
319
|
+
repo,
|
|
320
|
+
issue,
|
|
321
|
+
machine=machine,
|
|
322
|
+
model=model,
|
|
323
|
+
briefing_file=briefing_file,
|
|
324
|
+
do_plan=do_plan,
|
|
325
|
+
max_fix_rounds=max_fix_rounds,
|
|
326
|
+
skip_test=skip_test,
|
|
327
|
+
repo_path=repo_path,
|
|
328
|
+
poll=poll,
|
|
329
|
+
max_work_retries=max_work_retries,
|
|
330
|
+
deadline_mins=deadline_mins,
|
|
331
|
+
stall_mins=stall_mins,
|
|
332
|
+
notify=notify,
|
|
333
|
+
accept_advisory=accept_advisory,
|
|
334
|
+
force_review=force_review,
|
|
335
|
+
no_merge=no_merge,
|
|
336
|
+
merge_method=merge_method,
|
|
337
|
+
max_merge_attempts=max_merge_attempts,
|
|
338
|
+
dry_run=dry_run,
|
|
339
|
+
config_path=config_path,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
session = launch_drive_in_tmux(argv, repo=repo, issue=issue)
|
|
343
|
+
except DriveError as exc:
|
|
344
|
+
click.echo(f"✗ {exc}", err=True)
|
|
345
|
+
raise SystemExit(exc.exit_code) from None
|
|
346
|
+
click.echo(f"driving {repo} #{issue} in tmux session {session!r}")
|
|
347
|
+
click.echo(f" attach with: coord drive-attach {repo} {issue}")
|
|
348
|
+
click.echo(f" stop with: coord drive-stop {repo} {issue}")
|
|
349
|
+
raise SystemExit(0)
|
|
350
|
+
|
|
351
|
+
config = _load_config(config_path)
|
|
352
|
+
opts = DriveOptions(
|
|
353
|
+
machine=machine,
|
|
354
|
+
model=model,
|
|
355
|
+
briefing_file=briefing_file,
|
|
356
|
+
do_plan=do_plan,
|
|
357
|
+
max_fix_rounds=max_fix_rounds,
|
|
358
|
+
skip_test=skip_test,
|
|
359
|
+
repo_path=repo_path,
|
|
360
|
+
poll=poll,
|
|
361
|
+
max_work_retries=max_work_retries,
|
|
362
|
+
deadline_mins=deadline_mins,
|
|
363
|
+
stall_mins=stall_mins,
|
|
364
|
+
notify=notify,
|
|
365
|
+
do_merge=not no_merge,
|
|
366
|
+
merge_method=merge_method,
|
|
367
|
+
accept_advisory=accept_advisory,
|
|
368
|
+
force_review=force_review,
|
|
369
|
+
dry_run=dry_run,
|
|
370
|
+
max_merge_attempts=max_merge_attempts,
|
|
371
|
+
no_acceptance=no_acceptance,
|
|
372
|
+
config_path=str(config_path) if config_path else "",
|
|
373
|
+
)
|
|
374
|
+
driver = Driver(repo=repo, issue=issue, opts=opts, config=config)
|
|
375
|
+
try:
|
|
376
|
+
code = driver.run()
|
|
377
|
+
except DriveError as exc:
|
|
378
|
+
click.echo(f"✗ {exc}", err=True)
|
|
379
|
+
raise SystemExit(exc.exit_code) from None
|
|
380
|
+
except KeyboardInterrupt:
|
|
381
|
+
click.echo("interrupted", err=True)
|
|
382
|
+
raise SystemExit(130) from None
|
|
383
|
+
raise SystemExit(code)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ── `--tmux` companions: list / attach / stop (#1398) ─────────────────────────
|
|
387
|
+
#
|
|
388
|
+
# Three flat, hyphenated commands rather than a `drive` sub-group — `drive`
|
|
389
|
+
# itself already spends its position on `REPO ISSUE`, so a group would need
|
|
390
|
+
# a distinct verb anyway. Mirrors the naming of other hyphenated one-off
|
|
391
|
+
# commands in this CLI (`verify-merge`, `fix-briefing`, `approve-plan`).
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@click.command(
|
|
395
|
+
"drive-sessions",
|
|
396
|
+
help="List live `coord drive --tmux` sessions (coord-drive-<repo>-<issue>).",
|
|
397
|
+
)
|
|
398
|
+
@click.option(
|
|
399
|
+
"--json", "output_json", is_flag=True, default=False,
|
|
400
|
+
help="Output as JSON — an array of {repo, issue, session_name, attached}. "
|
|
401
|
+
"Consumed by coord-tui to badge Pipeline rows and gate the menu.",
|
|
402
|
+
)
|
|
403
|
+
def drive_sessions(output_json: bool) -> None:
|
|
404
|
+
import json as _json # noqa: PLC0415
|
|
405
|
+
|
|
406
|
+
from coord.drive import list_drive_sessions # noqa: PLC0415
|
|
407
|
+
|
|
408
|
+
entries = list_drive_sessions()
|
|
409
|
+
if output_json:
|
|
410
|
+
click.echo(_json.dumps(entries))
|
|
411
|
+
return
|
|
412
|
+
if not entries:
|
|
413
|
+
click.echo("No live drive sessions.")
|
|
414
|
+
return
|
|
415
|
+
for e in entries:
|
|
416
|
+
attached_tag = " [attached]" if e.get("attached") else ""
|
|
417
|
+
click.echo(f" {e['repo']} #{e['issue']}{attached_tag}")
|
|
418
|
+
click.echo(f" attach with: coord drive-attach {e['repo']} {e['issue']}")
|
|
419
|
+
click.echo(f" stop with: coord drive-stop {e['repo']} {e['issue']}")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@click.command(
|
|
423
|
+
"drive-attach",
|
|
424
|
+
help=(
|
|
425
|
+
"Attach to a live `coord drive --tmux` session for REPO ISSUE. "
|
|
426
|
+
"Type this into a local PTY exactly as `coord reattach`/`coord "
|
|
427
|
+
"terminal attach` are used for other session kinds."
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
@click.argument("repo")
|
|
431
|
+
@click.argument("issue", type=int)
|
|
432
|
+
def drive_attach(repo: str, issue: int) -> None:
|
|
433
|
+
from coord.drive import drive_session_name # noqa: PLC0415
|
|
434
|
+
from coord.interactive import TmuxHost, tmux_session_alive # noqa: PLC0415
|
|
435
|
+
|
|
436
|
+
host = TmuxHost(None)
|
|
437
|
+
session = drive_session_name(repo, issue)
|
|
438
|
+
if not tmux_session_alive(session, host=host):
|
|
439
|
+
click.echo(f"error: no live drive session for {repo} #{issue}.", err=True)
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
|
|
442
|
+
if os.environ.get("TMUX"):
|
|
443
|
+
# Already inside a tmux client — attach-session refuses to nest;
|
|
444
|
+
# switch-client moves the current client into the target session.
|
|
445
|
+
cmd = ["tmux", "switch-client", "-t", session]
|
|
446
|
+
else:
|
|
447
|
+
cmd = list(host.cmd(["attach-session", "-t", session], tty=True))
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
result = subprocess.run(cmd)
|
|
451
|
+
except (subprocess.SubprocessError, OSError) as exc:
|
|
452
|
+
click.echo(f"error: failed to attach: {exc}", err=True)
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
sys.exit(result.returncode)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@click.command(
|
|
458
|
+
"drive-stop",
|
|
459
|
+
help=(
|
|
460
|
+
"Stop a live `coord drive --tmux` run for REPO ISSUE by killing its "
|
|
461
|
+
"tmux session. This IS the correct cancellation: the per-issue flock "
|
|
462
|
+
"releases the instant the process exits, so the driver's own "
|
|
463
|
+
"already-driving guard never sees a stale lock."
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
@click.argument("repo")
|
|
467
|
+
@click.argument("issue", type=int)
|
|
468
|
+
def drive_stop(repo: str, issue: int) -> None:
|
|
469
|
+
from coord.drive import drive_session_name # noqa: PLC0415
|
|
470
|
+
from coord.interactive import TmuxHost, tmux_session_alive # noqa: PLC0415
|
|
471
|
+
|
|
472
|
+
host = TmuxHost(None)
|
|
473
|
+
session = drive_session_name(repo, issue)
|
|
474
|
+
if not tmux_session_alive(session, host=host):
|
|
475
|
+
click.echo(f"error: no live drive session for {repo} #{issue}.", err=True)
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
result = subprocess.run(
|
|
480
|
+
host.cmd(["kill-session", "-t", session]),
|
|
481
|
+
capture_output=True,
|
|
482
|
+
text=True,
|
|
483
|
+
timeout=10.0,
|
|
484
|
+
)
|
|
485
|
+
except (subprocess.SubprocessError, OSError) as exc:
|
|
486
|
+
click.echo(f"error: failed to kill tmux session: {exc}", err=True)
|
|
487
|
+
sys.exit(1)
|
|
488
|
+
if result.returncode != 0:
|
|
489
|
+
click.echo(
|
|
490
|
+
f"error: tmux kill-session failed: {(result.stderr or '').strip()}",
|
|
491
|
+
err=True,
|
|
492
|
+
)
|
|
493
|
+
sys.exit(1)
|
|
494
|
+
click.echo(f"Stopped driving {repo} #{issue}.")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ── driver escalation records (#1505) ────────────────────────────────────────
|
|
498
|
+
#
|
|
499
|
+
# `_decide_merge` writes one of these the moment the merge stage hits a
|
|
500
|
+
# status no amount of retrying can fix (NEEDS_ATTENTION / an unrecognised
|
|
501
|
+
# status) — see that function's docstring. `record` is the driver's own
|
|
502
|
+
# write (`Driver.run` executes it as the last thing before exiting, via
|
|
503
|
+
# `run_coord`, never as a direct DB call — same CLI-is-the-contract rule
|
|
504
|
+
# every other board mutation in this module follows). `run`/`dismiss`/`list`
|
|
505
|
+
# are the human's one-key responses, surfaced in coord-tui's Pipeline
|
|
506
|
+
# right-click menu (`run-escalation`/`dismiss-escalation`) and reachable
|
|
507
|
+
# from any shell.
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@click.group("escalate")
|
|
511
|
+
def escalate_group() -> None:
|
|
512
|
+
"""Board-visible "driver stuck" records the merge stage of `coord drive`
|
|
513
|
+
writes instead of burning the retry budget on a status retrying can't
|
|
514
|
+
fix (#1505). `record` is written by the driver itself; the human answers
|
|
515
|
+
with `run` (execute the proposed fix), `dismiss` (clear the record), or
|
|
516
|
+
`list` (see what's outstanding).
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@escalate_group.command("record")
|
|
521
|
+
@click.argument("repo")
|
|
522
|
+
@click.argument("issue", type=int)
|
|
523
|
+
@click.option("--stage", default="merge", show_default=True, help="Pipeline stage that escalated.")
|
|
524
|
+
@click.option("--reason", required=True, help="Why the driver stopped.")
|
|
525
|
+
@click.option(
|
|
526
|
+
"--gate", "gates", multiple=True,
|
|
527
|
+
help="One observed gate reading as key=value. Repeatable.",
|
|
528
|
+
)
|
|
529
|
+
@click.option("--command", "proposed_command", required=True, help="The proposed fix, as a shell command.")
|
|
530
|
+
@click.option("--assignment", "assignment_id", default=None, help="The merge-queue/work assignment id, if known.")
|
|
531
|
+
@_CONFIG_OPTION
|
|
532
|
+
def escalate_record(
|
|
533
|
+
repo: str,
|
|
534
|
+
issue: int,
|
|
535
|
+
stage: str,
|
|
536
|
+
reason: str,
|
|
537
|
+
gates: tuple[str, ...],
|
|
538
|
+
proposed_command: str,
|
|
539
|
+
assignment_id: str | None,
|
|
540
|
+
config_path: Path,
|
|
541
|
+
) -> None:
|
|
542
|
+
"""Write (or replace) the escalation record for REPO ISSUE."""
|
|
543
|
+
from coord.state import record_drive_escalation # noqa: PLC0415
|
|
544
|
+
|
|
545
|
+
record_drive_escalation(
|
|
546
|
+
repo,
|
|
547
|
+
issue,
|
|
548
|
+
stage=stage,
|
|
549
|
+
reason=reason,
|
|
550
|
+
gate_readings=" | ".join(gates),
|
|
551
|
+
proposed_command=proposed_command,
|
|
552
|
+
assignment_id=assignment_id,
|
|
553
|
+
)
|
|
554
|
+
click.echo(f"escalation recorded for {repo} #{issue}: {reason}")
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@escalate_group.command("dismiss")
|
|
558
|
+
@click.argument("repo")
|
|
559
|
+
@click.argument("issue", type=int)
|
|
560
|
+
@_CONFIG_OPTION
|
|
561
|
+
def escalate_dismiss(repo: str, issue: int, config_path: Path) -> None:
|
|
562
|
+
"""Clear the escalation record for REPO ISSUE without acting on it."""
|
|
563
|
+
from coord.state import dismiss_drive_escalation # noqa: PLC0415
|
|
564
|
+
|
|
565
|
+
ok = dismiss_drive_escalation(repo, issue)
|
|
566
|
+
click.echo(f"dismissed escalation for {repo} #{issue}" if ok else "no escalation on file")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@escalate_group.command("list")
|
|
570
|
+
@click.option("--repo", "repo", default=None, help="Restrict to one repo (default: every repo).")
|
|
571
|
+
@_CONFIG_OPTION
|
|
572
|
+
def escalate_list(repo: str | None, config_path: Path) -> None:
|
|
573
|
+
"""List every open escalation record."""
|
|
574
|
+
from coord.state import list_drive_escalations # noqa: PLC0415
|
|
575
|
+
|
|
576
|
+
entries = list_drive_escalations(repo)
|
|
577
|
+
if not entries:
|
|
578
|
+
click.echo("(no open escalations)")
|
|
579
|
+
return
|
|
580
|
+
for e in entries:
|
|
581
|
+
click.echo(f"{e['repo_name']} #{e['issue_number']} [{e['stage']}]: {e['reason']}")
|
|
582
|
+
if e.get("gate_readings"):
|
|
583
|
+
click.echo(f" gates: {e['gate_readings']}")
|
|
584
|
+
click.echo(f" proposed: {e['proposed_command']}")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@escalate_group.command("run")
|
|
588
|
+
@click.argument("repo")
|
|
589
|
+
@click.argument("issue", type=int)
|
|
590
|
+
@click.option(
|
|
591
|
+
"--dismiss/--no-dismiss", default=True,
|
|
592
|
+
help="Clear the record after the proposed command exits 0 (default: yes).",
|
|
593
|
+
)
|
|
594
|
+
@_CONFIG_OPTION
|
|
595
|
+
def escalate_run(repo: str, issue: int, dismiss: bool, config_path: Path) -> None:
|
|
596
|
+
"""Run the proposed fix command for REPO ISSUE's escalation.
|
|
597
|
+
|
|
598
|
+
This IS the "one-key human decision" the escalation record exists for —
|
|
599
|
+
nothing runs it automatically; a human (or the TUI's "Run it" menu item,
|
|
600
|
+
which shells out to exactly this) has to explicitly ask for it.
|
|
601
|
+
"""
|
|
602
|
+
from coord.state import dismiss_drive_escalation, get_drive_escalation # noqa: PLC0415
|
|
603
|
+
|
|
604
|
+
entry = get_drive_escalation(repo, issue)
|
|
605
|
+
if entry is None:
|
|
606
|
+
raise click.ClickException(f"no escalation on file for {repo} #{issue}")
|
|
607
|
+
command = (entry.get("proposed_command") or "").strip()
|
|
608
|
+
if not command:
|
|
609
|
+
raise click.ClickException("escalation record has no proposed command")
|
|
610
|
+
click.echo(f"running: {command}")
|
|
611
|
+
result = subprocess.run(command, shell=True) # noqa: S602 — operator-approved, one-key by design
|
|
612
|
+
if result.returncode != 0:
|
|
613
|
+
raise click.ClickException(f"proposed command exited {result.returncode}")
|
|
614
|
+
if dismiss:
|
|
615
|
+
dismiss_drive_escalation(repo, issue)
|
|
616
|
+
click.echo("done")
|