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/issues.py
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""`coord sync` plus the `issue`/`context` groups and `track`/`untrack`/
|
|
2
|
+
`backlog`. Extracted from coord/cli.py (#747)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from coord.commands._common import _apply_label_change, _CONFIG_OPTION, _load_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command(help="Sync open issues from GitHub into the local SQLite cache.")
|
|
15
|
+
@_CONFIG_OPTION
|
|
16
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress per-repo output.")
|
|
17
|
+
def sync(config_path: Path, quiet: bool) -> None:
|
|
18
|
+
"""Fetch open issues for every configured repo and write them to the local
|
|
19
|
+
``issues`` table in ``~/.coord/coord.db``.
|
|
20
|
+
|
|
21
|
+
The TUI board reads from this table to show the full backlog under
|
|
22
|
+
Pending. Run this manually, call it from a cron job, or press 'r' in
|
|
23
|
+
the TUI which triggers it automatically alongside the data refresh.
|
|
24
|
+
"""
|
|
25
|
+
from coord import github_ops
|
|
26
|
+
from coord.state import (
|
|
27
|
+
list_issue_numbers_with_assignments,
|
|
28
|
+
sync_issue_comments,
|
|
29
|
+
upsert_open_issues,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
cfg = _load_config(config_path)
|
|
33
|
+
total = 0
|
|
34
|
+
for repo in cfg.repos:
|
|
35
|
+
try:
|
|
36
|
+
issues = github_ops.get_open_issues(repo.github)
|
|
37
|
+
upsert_open_issues(repo.name, issues)
|
|
38
|
+
if not quiet:
|
|
39
|
+
click.echo(f" {repo.name}: {len(issues)} open issue(s)")
|
|
40
|
+
total += len(issues)
|
|
41
|
+
except Exception as e: # noqa: BLE001
|
|
42
|
+
click.echo(f" {repo.name}: sync failed — {e}", err=True)
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
# #873: opportunistically backfill the durable issue_comments mirror
|
|
46
|
+
# for issues coord has actually dispatched work on — scoped (not a
|
|
47
|
+
# crawl of every open issue) to keep this bounded on every sync.
|
|
48
|
+
assigned_numbers = list_issue_numbers_with_assignments(repo.name)
|
|
49
|
+
open_numbers = {i["number"] for i in issues}
|
|
50
|
+
comments_synced = 0
|
|
51
|
+
for number in sorted(assigned_numbers & open_numbers):
|
|
52
|
+
try:
|
|
53
|
+
comments_synced += sync_issue_comments(
|
|
54
|
+
repo.name, number, repo_github=repo.github
|
|
55
|
+
)
|
|
56
|
+
except Exception: # noqa: BLE001 — best-effort, never fails `coord sync`
|
|
57
|
+
pass
|
|
58
|
+
if not quiet and assigned_numbers & open_numbers:
|
|
59
|
+
click.echo(
|
|
60
|
+
f" {repo.name}: {comments_synced} comment(s) synced across "
|
|
61
|
+
f"{len(assigned_numbers & open_numbers)} assigned issue(s)"
|
|
62
|
+
)
|
|
63
|
+
if not quiet:
|
|
64
|
+
click.echo(f"synced {total} open issue(s) across {len(cfg.repos)} repo(s)")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.group("issue")
|
|
68
|
+
def issue_group() -> None:
|
|
69
|
+
"""Issue-tracker operations through the backend-agnostic seam.
|
|
70
|
+
|
|
71
|
+
The write routes through the daemon (GitHub via `gh` today; GitLab /
|
|
72
|
+
bare-DB later) so callers — notably the chat-about-issue session — never
|
|
73
|
+
touch `gh` directly.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@issue_group.command(
|
|
78
|
+
"edit",
|
|
79
|
+
help=(
|
|
80
|
+
"Edit an issue's title and/or body. REPO is the local repo name from "
|
|
81
|
+
"coordinator.yml; ISSUE is the GH issue number. Provide --title and/or "
|
|
82
|
+
"--body / --body-file. Routes through the issue-tracker seam."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@click.argument("repo")
|
|
88
|
+
@click.argument("issue", type=int)
|
|
89
|
+
@click.option("--title", default=None, help="New issue title.")
|
|
90
|
+
@click.option("--body", default=None, help="New issue body (markdown).")
|
|
91
|
+
@click.option(
|
|
92
|
+
"--body-file",
|
|
93
|
+
type=click.Path(path_type=Path),
|
|
94
|
+
default=None,
|
|
95
|
+
help="Read the new body from a file (preferred for long markdown). '-' = stdin.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@_CONFIG_OPTION
|
|
100
|
+
def issue_edit_cmd(
|
|
101
|
+
repo: str,
|
|
102
|
+
issue: int,
|
|
103
|
+
title: str | None,
|
|
104
|
+
body: str | None,
|
|
105
|
+
body_file: Path | None,
|
|
106
|
+
config_path: Path,
|
|
107
|
+
) -> None:
|
|
108
|
+
cfg = _load_config(config_path)
|
|
109
|
+
repo_entry = cfg.repo(repo)
|
|
110
|
+
slug = repo_entry.github if repo_entry else repo
|
|
111
|
+
if body_file is not None:
|
|
112
|
+
body = sys.stdin.read() if str(body_file) == "-" else Path(body_file).read_text()
|
|
113
|
+
if title is None and body is None:
|
|
114
|
+
click.echo("error: provide --title and/or --body / --body-file", err=True)
|
|
115
|
+
sys.exit(2)
|
|
116
|
+
from coord.state import edit_issue_content # noqa: PLC0415
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
updated = edit_issue_content(
|
|
120
|
+
repo, issue, title=title, body=body, repo_github=slug
|
|
121
|
+
)
|
|
122
|
+
except Exception as e: # noqa: BLE001
|
|
123
|
+
click.echo(f"error: issue edit failed: {e}", err=True)
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
click.echo(f"#{issue} ({slug}) updated" if updated else f"#{issue} ({slug}): no change")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@issue_group.command(
|
|
129
|
+
"close",
|
|
130
|
+
help=(
|
|
131
|
+
"Close an issue, optionally posting --comment first (#1003). REPO "
|
|
132
|
+
"is the local repo name from coordinator.yml; ISSUE is the GH issue "
|
|
133
|
+
"number. The Plans-panel \"Close / archive plan\" action's backend "
|
|
134
|
+
"— thin wrapper around the already-existing "
|
|
135
|
+
"``github_ops.close_issue()`` (previously only called internally by "
|
|
136
|
+
"``coord merge``), now operator-exposed through the tracker seam. "
|
|
137
|
+
"Idempotent — closing an already-closed issue is a no-op. "
|
|
138
|
+
"#1196: refuses (exit 1, clear message) when the issue still has "
|
|
139
|
+
"open children — an epic must not read as \"done\" while its "
|
|
140
|
+
"sub-issues are open/unstarted. Pass --force to override, "
|
|
141
|
+
"mirroring `coord merge --force-merge`."
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
@click.argument("repo")
|
|
145
|
+
@click.argument("issue", type=int)
|
|
146
|
+
@click.option(
|
|
147
|
+
"--comment", default=None, help="Comment to post before closing (markdown)."
|
|
148
|
+
)
|
|
149
|
+
@click.option(
|
|
150
|
+
"--force", is_flag=True, default=False,
|
|
151
|
+
help="Close even if the issue has open children (#1196).",
|
|
152
|
+
)
|
|
153
|
+
@_CONFIG_OPTION
|
|
154
|
+
def issue_close_cmd(
|
|
155
|
+
repo: str,
|
|
156
|
+
issue: int,
|
|
157
|
+
comment: str | None,
|
|
158
|
+
force: bool,
|
|
159
|
+
config_path: Path,
|
|
160
|
+
) -> None:
|
|
161
|
+
cfg = _load_config(config_path)
|
|
162
|
+
repo_entry = cfg.repo(repo)
|
|
163
|
+
slug = repo_entry.github if repo_entry else repo
|
|
164
|
+
from coord.state import close_issue # noqa: PLC0415
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
close_issue(repo, issue, comment=comment, repo_github=slug, force=force)
|
|
168
|
+
except Exception as e: # noqa: BLE001
|
|
169
|
+
click.echo(f"error: issue close failed: {e}", err=True)
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
click.echo(f"#{issue} ({slug}) closed")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@issue_group.command(
|
|
175
|
+
"reopen",
|
|
176
|
+
help=(
|
|
177
|
+
"Reopen a closed issue, optionally posting --comment first (#1078). "
|
|
178
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the GH "
|
|
179
|
+
"issue number. Mirror of the `close` command for the complement "
|
|
180
|
+
"operation. Idempotent — reopening an already-open issue is a no-op."
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
@click.argument("repo")
|
|
184
|
+
@click.argument("issue", type=int)
|
|
185
|
+
@click.option(
|
|
186
|
+
"--comment", default=None, help="Comment to post before reopening (markdown)."
|
|
187
|
+
)
|
|
188
|
+
@_CONFIG_OPTION
|
|
189
|
+
def issue_reopen_cmd(
|
|
190
|
+
repo: str,
|
|
191
|
+
issue: int,
|
|
192
|
+
comment: str | None,
|
|
193
|
+
config_path: Path,
|
|
194
|
+
) -> None:
|
|
195
|
+
cfg = _load_config(config_path)
|
|
196
|
+
repo_entry = cfg.repo(repo)
|
|
197
|
+
slug = repo_entry.github if repo_entry else repo
|
|
198
|
+
from coord.state import reopen_issue # noqa: PLC0415
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
reopen_issue(repo, issue, comment=comment, repo_github=slug)
|
|
202
|
+
except Exception as e: # noqa: BLE001
|
|
203
|
+
click.echo(f"error: issue reopen failed: {e}", err=True)
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
click.echo(f"#{issue} ({slug}) reopened")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@issue_group.command(
|
|
209
|
+
"create",
|
|
210
|
+
help=(
|
|
211
|
+
"Create a new GitHub issue through the backend-agnostic seam. REPO "
|
|
212
|
+
"is the local repo name from coordinator.yml. Prints the new issue "
|
|
213
|
+
"number on success.\n\n"
|
|
214
|
+
"Use --body-file for long markdown bodies (avoids shell-quoting "
|
|
215
|
+
"issues). '-' reads from stdin. Routes through the daemon seam so "
|
|
216
|
+
"agents never need to call `gh issue create` directly."
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
@click.argument("repo")
|
|
220
|
+
@click.option("--title", required=True, help="Issue title.")
|
|
221
|
+
@click.option("--body", default=None, help="Issue body (markdown).")
|
|
222
|
+
@click.option(
|
|
223
|
+
"--body-file",
|
|
224
|
+
type=click.Path(path_type=Path),
|
|
225
|
+
default=None,
|
|
226
|
+
help="Read the body from a file. '-' = stdin.",
|
|
227
|
+
)
|
|
228
|
+
@click.option(
|
|
229
|
+
"--label",
|
|
230
|
+
"labels",
|
|
231
|
+
multiple=True,
|
|
232
|
+
help="Label to add (repeatable). The label must already exist in the repo.",
|
|
233
|
+
)
|
|
234
|
+
@_CONFIG_OPTION
|
|
235
|
+
def issue_create_cmd(
|
|
236
|
+
repo: str,
|
|
237
|
+
title: str,
|
|
238
|
+
body: str | None,
|
|
239
|
+
body_file: Path | None,
|
|
240
|
+
labels: tuple[str, ...],
|
|
241
|
+
config_path: Path,
|
|
242
|
+
) -> None:
|
|
243
|
+
cfg = _load_config(config_path)
|
|
244
|
+
repo_entry = cfg.repo(repo)
|
|
245
|
+
slug = repo_entry.github if repo_entry else repo
|
|
246
|
+
if body_file is not None:
|
|
247
|
+
body = sys.stdin.read() if str(body_file) == "-" else Path(body_file).read_text()
|
|
248
|
+
from coord.state import create_issue as _create_issue # noqa: PLC0415
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
result = _create_issue(
|
|
252
|
+
repo, title, body or "",
|
|
253
|
+
labels=list(labels),
|
|
254
|
+
repo_github=slug,
|
|
255
|
+
)
|
|
256
|
+
except Exception as e: # noqa: BLE001
|
|
257
|
+
click.echo(f"error: issue create failed: {e}", err=True)
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
click.echo(f"#{result['number']} ({slug}) created")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@issue_group.command(
|
|
263
|
+
"label",
|
|
264
|
+
help=(
|
|
265
|
+
"Add and/or remove arbitrary labels on an existing issue through the "
|
|
266
|
+
"backend-agnostic seam. REPO is the local repo name from "
|
|
267
|
+
"coordinator.yml; ISSUE is the GH issue number.\n\n"
|
|
268
|
+
"Provide --add and/or --remove (both repeatable). Already-present "
|
|
269
|
+
"labels in --add and already-absent labels in --remove are "
|
|
270
|
+
"silently ignored (idempotent). Updates the local issues cache so "
|
|
271
|
+
"the TUI reflects the change without waiting for `coord sync`.\n\n"
|
|
272
|
+
"Routes through the daemon seam so agents never need to call "
|
|
273
|
+
"`gh issue edit` directly."
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
@click.argument("repo")
|
|
277
|
+
@click.argument("issue", type=int)
|
|
278
|
+
@click.option(
|
|
279
|
+
"--add",
|
|
280
|
+
"add_labels",
|
|
281
|
+
multiple=True,
|
|
282
|
+
help="Label to add (repeatable).",
|
|
283
|
+
)
|
|
284
|
+
@click.option(
|
|
285
|
+
"--remove",
|
|
286
|
+
"remove_labels",
|
|
287
|
+
multiple=True,
|
|
288
|
+
help="Label to remove (repeatable).",
|
|
289
|
+
)
|
|
290
|
+
@_CONFIG_OPTION
|
|
291
|
+
def issue_label_cmd(
|
|
292
|
+
repo: str,
|
|
293
|
+
issue: int,
|
|
294
|
+
add_labels: tuple[str, ...],
|
|
295
|
+
remove_labels: tuple[str, ...],
|
|
296
|
+
config_path: Path,
|
|
297
|
+
) -> None:
|
|
298
|
+
if not add_labels and not remove_labels:
|
|
299
|
+
click.echo("error: provide --add and/or --remove", err=True)
|
|
300
|
+
sys.exit(2)
|
|
301
|
+
from coord.state import apply_issue_labels, get_cached_issue_labels # noqa: PLC0415
|
|
302
|
+
|
|
303
|
+
cfg = _load_config(config_path)
|
|
304
|
+
repo_entry = cfg.repo(repo)
|
|
305
|
+
slug = repo_entry.github if repo_entry else repo
|
|
306
|
+
|
|
307
|
+
# Snapshot the pre-change cache so the echoed message reflects the actual
|
|
308
|
+
# delta, not just the requested --add/--remove sets (a requested label
|
|
309
|
+
# that was already present/absent is a no-op for it specifically, even
|
|
310
|
+
# when other labels in the same call do change something).
|
|
311
|
+
old_labels = get_cached_issue_labels(repo, issue)
|
|
312
|
+
try:
|
|
313
|
+
new_labels, changed = apply_issue_labels(
|
|
314
|
+
repo, issue,
|
|
315
|
+
add=set(add_labels),
|
|
316
|
+
remove=set(remove_labels),
|
|
317
|
+
repo_github=slug,
|
|
318
|
+
)
|
|
319
|
+
except Exception as e: # noqa: BLE001
|
|
320
|
+
click.echo(f"error: issue label failed: {e}", err=True)
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
if changed:
|
|
323
|
+
if old_labels is not None:
|
|
324
|
+
actually_added = sorted(set(add_labels) & (set(new_labels) - set(old_labels)))
|
|
325
|
+
actually_removed = sorted(set(remove_labels) & (set(old_labels) - set(new_labels)))
|
|
326
|
+
else:
|
|
327
|
+
# No prior cache snapshot to diff against (issue not yet synced
|
|
328
|
+
# locally) — fall back to the requested sets.
|
|
329
|
+
actually_added = sorted(add_labels)
|
|
330
|
+
actually_removed = sorted(remove_labels)
|
|
331
|
+
parts: list[str] = []
|
|
332
|
+
if actually_added:
|
|
333
|
+
parts.append(f"+{{{', '.join(actually_added)}}}")
|
|
334
|
+
if actually_removed:
|
|
335
|
+
parts.append(f"-{{{', '.join(actually_removed)}}}")
|
|
336
|
+
if not parts:
|
|
337
|
+
# Stale local cache made the diff a no-op even though the seam
|
|
338
|
+
# reported a real change upstream — fall back to the full
|
|
339
|
+
# resulting label set rather than echoing a blank delta.
|
|
340
|
+
parts.append(f"(now: {{{', '.join(sorted(new_labels))}}})")
|
|
341
|
+
click.echo(f"#{issue} ({slug}) labels updated: {' '.join(parts)}")
|
|
342
|
+
else:
|
|
343
|
+
click.echo(f"#{issue} ({slug}) labels unchanged (no delta)")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@click.group("context")
|
|
347
|
+
def context_group() -> None:
|
|
348
|
+
"""The per-issue rolling context digest (#603).
|
|
349
|
+
|
|
350
|
+
Short, curated notes (cross-repo deps, approaches already tried, hard
|
|
351
|
+
constraints) injected at the TOP of every agent briefing for the issue so
|
|
352
|
+
findings don't evaporate between attempts. DB-only, dropped when the issue
|
|
353
|
+
closes. Pinned entries stay on top and never age out.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@context_group.command("show")
|
|
358
|
+
@click.argument("repo")
|
|
359
|
+
@click.argument("issue", type=int)
|
|
360
|
+
@_CONFIG_OPTION
|
|
361
|
+
def context_show(repo: str, issue: int, config_path: Path) -> None:
|
|
362
|
+
"""Print the rendered digest plus raw entries (with ids for pin/clear)."""
|
|
363
|
+
from coord.state import list_issue_context, render_issue_context_entries
|
|
364
|
+
|
|
365
|
+
entries = list_issue_context(repo, issue)
|
|
366
|
+
if not entries:
|
|
367
|
+
click.echo(f"(no context for {repo} #{issue})")
|
|
368
|
+
return
|
|
369
|
+
click.echo(render_issue_context_entries(entries))
|
|
370
|
+
click.echo("\nentries (id · source · pinned):")
|
|
371
|
+
for e in entries:
|
|
372
|
+
pin = "📌" if e["pinned"] else " "
|
|
373
|
+
src = f" [{e['source']}]" if e.get("source") else ""
|
|
374
|
+
click.echo(f" {pin} #{e['id']}{src}: {e['body']}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@context_group.command("add")
|
|
378
|
+
@click.argument("repo")
|
|
379
|
+
@click.argument("issue", type=int)
|
|
380
|
+
@click.argument("body")
|
|
381
|
+
@click.option(
|
|
382
|
+
"--pin", "pinned", is_flag=True,
|
|
383
|
+
help="Pin as a critical (always on top, never aged out by the budget).",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@click.option(
|
|
388
|
+
"--source", default="operator",
|
|
389
|
+
help="Who recorded this: work|fix|review|test|operator (default operator).",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@_CONFIG_OPTION
|
|
394
|
+
def context_add(
|
|
395
|
+
repo: str, issue: int, body: str, pinned: bool, source: str, config_path: Path
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Append a context entry for REPO #ISSUE (BODY is one short finding)."""
|
|
398
|
+
from coord.state import add_issue_context_entry
|
|
399
|
+
|
|
400
|
+
eid = add_issue_context_entry(repo, issue, body, pinned=pinned, source=source)
|
|
401
|
+
tag = " (pinned)" if pinned else ""
|
|
402
|
+
suffix = f" (id {eid})" if eid else ""
|
|
403
|
+
click.echo(f"added{tag} to {repo} #{issue}{suffix}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@context_group.command("pin")
|
|
407
|
+
@click.argument("repo")
|
|
408
|
+
@click.argument("issue", type=int)
|
|
409
|
+
@click.argument("entry_id", type=int)
|
|
410
|
+
@_CONFIG_OPTION
|
|
411
|
+
def context_pin(repo: str, issue: int, entry_id: int, config_path: Path) -> None:
|
|
412
|
+
"""Pin entry ENTRY_ID so it stays on top and never ages out."""
|
|
413
|
+
from coord.state import set_issue_context_pin
|
|
414
|
+
|
|
415
|
+
click.echo("pinned" if set_issue_context_pin(repo, issue, entry_id, True) else "no such entry")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@context_group.command("unpin")
|
|
419
|
+
@click.argument("repo")
|
|
420
|
+
@click.argument("issue", type=int)
|
|
421
|
+
@click.argument("entry_id", type=int)
|
|
422
|
+
@_CONFIG_OPTION
|
|
423
|
+
def context_unpin(repo: str, issue: int, entry_id: int, config_path: Path) -> None:
|
|
424
|
+
"""Unpin entry ENTRY_ID (it becomes a normal aged-out note)."""
|
|
425
|
+
from coord.state import set_issue_context_pin
|
|
426
|
+
|
|
427
|
+
click.echo("unpinned" if set_issue_context_pin(repo, issue, entry_id, False) else "no such entry")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@context_group.command("clear")
|
|
431
|
+
@click.argument("repo")
|
|
432
|
+
@click.argument("issue", type=int)
|
|
433
|
+
@_CONFIG_OPTION
|
|
434
|
+
def context_clear(repo: str, issue: int, config_path: Path) -> None:
|
|
435
|
+
"""Delete ALL context entries for REPO #ISSUE."""
|
|
436
|
+
from coord.state import clear_issue_context
|
|
437
|
+
|
|
438
|
+
n = clear_issue_context(repo, issue)
|
|
439
|
+
click.echo(f"cleared {n} entr{'y' if n == 1 else 'ies'} for {repo} #{issue}")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@context_group.command("curate")
|
|
443
|
+
@click.argument("repo")
|
|
444
|
+
@click.argument("issue", type=int)
|
|
445
|
+
@click.option(
|
|
446
|
+
"--model", default="haiku",
|
|
447
|
+
help="claude -p model for the compress (default haiku — cheap).",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@_CONFIG_OPTION
|
|
452
|
+
def context_curate(repo: str, issue: int, model: str, config_path: Path) -> None:
|
|
453
|
+
"""LLM-compress the digest: merge duplicates, drop resolved notes, keep
|
|
454
|
+
pinned criticals. On-demand (one metered `claude -p` call) — the everyday
|
|
455
|
+
cap+pins curation is automatic and free."""
|
|
456
|
+
import json as _json
|
|
457
|
+
import re as _re
|
|
458
|
+
|
|
459
|
+
from coord.state import list_issue_context, replace_issue_context
|
|
460
|
+
from coord.test_orchestrator import _call_claude
|
|
461
|
+
|
|
462
|
+
entries = list_issue_context(repo, issue)
|
|
463
|
+
if len(entries) <= 3:
|
|
464
|
+
click.echo(f"{repo} #{issue}: {len(entries)} entries — nothing to curate.")
|
|
465
|
+
return
|
|
466
|
+
payload = _json.dumps(
|
|
467
|
+
[{"body": e["body"], "pinned": e["pinned"], "source": e.get("source")}
|
|
468
|
+
for e in entries],
|
|
469
|
+
indent=2,
|
|
470
|
+
)
|
|
471
|
+
system = (
|
|
472
|
+
"You compress a SHORT per-issue engineering context digest injected at "
|
|
473
|
+
"the top of an AI agent's briefing. Rules: merge duplicates; drop "
|
|
474
|
+
"resolved / obsolete / now-irrelevant notes; KEEP every cross-repo "
|
|
475
|
+
"dependency, hard constraint, and failed-approach lesson; never invent "
|
|
476
|
+
"facts. Preserve pinned=true for criticals (deps/constraints). Aim for "
|
|
477
|
+
"<= 8 entries, each one tight line. Output ONLY a JSON array of "
|
|
478
|
+
'{"body": str, "pinned": bool} — no prose, no code fences.'
|
|
479
|
+
)
|
|
480
|
+
try:
|
|
481
|
+
raw = _call_claude(system, payload, model=model)
|
|
482
|
+
except Exception as exc: # noqa: BLE001
|
|
483
|
+
click.echo(f"error: curate failed: {exc}", err=True)
|
|
484
|
+
sys.exit(1)
|
|
485
|
+
match = _re.search(r"\[.*\]", raw, _re.DOTALL)
|
|
486
|
+
try:
|
|
487
|
+
parsed = _json.loads(match.group(0)) if match else None
|
|
488
|
+
assert isinstance(parsed, list)
|
|
489
|
+
except Exception: # noqa: BLE001
|
|
490
|
+
click.echo(
|
|
491
|
+
"error: curate returned unparseable output; context left unchanged.",
|
|
492
|
+
err=True,
|
|
493
|
+
)
|
|
494
|
+
sys.exit(1)
|
|
495
|
+
cleaned = [
|
|
496
|
+
{"body": str(e.get("body", "")).strip(),
|
|
497
|
+
"pinned": bool(e.get("pinned")), "source": "curated"}
|
|
498
|
+
for e in parsed
|
|
499
|
+
if str(e.get("body", "")).strip()
|
|
500
|
+
]
|
|
501
|
+
if not cleaned:
|
|
502
|
+
click.echo(
|
|
503
|
+
"error: curate produced no entries; context left unchanged.", err=True
|
|
504
|
+
)
|
|
505
|
+
sys.exit(1)
|
|
506
|
+
replace_issue_context(repo, issue, cleaned)
|
|
507
|
+
click.echo(f"curated {repo} #{issue}: {len(entries)} → {len(cleaned)} entries")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@click.command(
|
|
511
|
+
help=(
|
|
512
|
+
"Send an issue to the Pipeline as DISPATCHABLE by tagging it with "
|
|
513
|
+
"both the `coord` and `status:ready` labels on GitHub.\n\n"
|
|
514
|
+
"A dispatchable Pipeline:New card needs BOTH labels. Coordinator "
|
|
515
|
+
"issues are often *created* with `coord` already, so adding only "
|
|
516
|
+
"`coord` was a no-op that left them stuck without `status:ready` "
|
|
517
|
+
"(#486 Leg 4 bug). This now ensures both — idempotent: in the normal "
|
|
518
|
+
"Refining → Refined (`coord ready`) → Send flow the issue already has "
|
|
519
|
+
"`status:ready`, so only `coord` is added. Any pre-Pipeline "
|
|
520
|
+
"`status:refining` / `status:backlog` label is cleared, mirroring "
|
|
521
|
+
"`coord ready`.\n\n"
|
|
522
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the "
|
|
523
|
+
"GH issue number."
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@click.argument("repo")
|
|
529
|
+
@click.argument("issue", type=int)
|
|
530
|
+
@_CONFIG_OPTION
|
|
531
|
+
def track(repo: str, issue: int, config_path: Path) -> None:
|
|
532
|
+
"""#261/#486: TUI right-click 'Send to Pipeline' fires this command to
|
|
533
|
+
make the issue a dispatchable Pipeline:New card (`coord` + `status:ready`)."""
|
|
534
|
+
cfg = _load_config(config_path)
|
|
535
|
+
repo_entry = cfg.repo(repo)
|
|
536
|
+
slug = repo_entry.github if repo_entry else repo
|
|
537
|
+
_apply_label_change(
|
|
538
|
+
repo, issue, config_path,
|
|
539
|
+
add={"coord", "status:ready"},
|
|
540
|
+
remove_if_present={"status:refining", "status:backlog"},
|
|
541
|
+
success_message=(
|
|
542
|
+
f"#{issue} ({slug}) sent to Pipeline (coord + status:ready)"
|
|
543
|
+
),
|
|
544
|
+
no_op_message=(
|
|
545
|
+
f"#{issue} ({slug}) already dispatchable "
|
|
546
|
+
"(coord + status:ready present)"
|
|
547
|
+
),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@click.command(
|
|
552
|
+
help=(
|
|
553
|
+
"Remove an issue from the Pipeline, returning it to the Board's "
|
|
554
|
+
"Backlog. Strips the `coord` label (Pipeline membership is the "
|
|
555
|
+
"`coord` label, so this is the only way to evict a card) plus any "
|
|
556
|
+
"`status:*` label, so the issue lands in Backlog rather than "
|
|
557
|
+
"Refined/Refining.\n\n"
|
|
558
|
+
"Inverse of `coord track` (Send to Pipeline). The TUI right-click "
|
|
559
|
+
"'Drop to backlog' on a Pipeline row fires this.\n\n"
|
|
560
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the "
|
|
561
|
+
"GH issue number."
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@click.argument("repo")
|
|
567
|
+
@click.argument("issue", type=int)
|
|
568
|
+
@_CONFIG_OPTION
|
|
569
|
+
def untrack(repo: str, issue: int, config_path: Path) -> None:
|
|
570
|
+
"""#266: TUI right-click 'Drop to backlog' on a Pipeline row fires this to
|
|
571
|
+
evict the issue from the coord Pipeline (removes `coord` + any `status:*`)."""
|
|
572
|
+
cfg = _load_config(config_path)
|
|
573
|
+
repo_entry = cfg.repo(repo)
|
|
574
|
+
slug = repo_entry.github if repo_entry else repo
|
|
575
|
+
_apply_label_change(
|
|
576
|
+
repo, issue, config_path,
|
|
577
|
+
add=set(),
|
|
578
|
+
# #1500: also strip `status:queued` (see `backlog`'s identical
|
|
579
|
+
# note) — dropping a staged card out of the Pipeline entirely must
|
|
580
|
+
# not leave the marker behind to silently re-surface on a later
|
|
581
|
+
# `coord track`.
|
|
582
|
+
remove_if_present={
|
|
583
|
+
"coord", "status:ready", "status:refining", "status:backlog",
|
|
584
|
+
"status:queued",
|
|
585
|
+
},
|
|
586
|
+
success_message=f"#{issue} ({slug}) dropped to Backlog (removed from Pipeline)",
|
|
587
|
+
no_op_message=f"#{issue} ({slug}) not in the Pipeline (no coord label)",
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@click.command(
|
|
592
|
+
help=(
|
|
593
|
+
"Drop an issue back to Backlog by removing its `status:*` label.\n\n"
|
|
594
|
+
"Symmetric with `coord refine` / `coord ready` — strips both "
|
|
595
|
+
"`status:refining` and `status:ready` if present, returning the "
|
|
596
|
+
"issue to the unscoped Backlog state.\n\n"
|
|
597
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the "
|
|
598
|
+
"GH issue number."
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@click.argument("repo")
|
|
604
|
+
@click.argument("issue", type=int)
|
|
605
|
+
@_CONFIG_OPTION
|
|
606
|
+
def backlog(repo: str, issue: int, config_path: Path) -> None:
|
|
607
|
+
"""#266: TUI right-click 'Drop to Backlog' fires this command to
|
|
608
|
+
walk a Refining/Refined row back to the unscoped Backlog state."""
|
|
609
|
+
cfg = _load_config(config_path)
|
|
610
|
+
repo_entry = cfg.repo(repo)
|
|
611
|
+
slug = repo_entry.github if repo_entry else repo
|
|
612
|
+
_apply_label_change(
|
|
613
|
+
repo, issue, config_path,
|
|
614
|
+
add=set(),
|
|
615
|
+
# #1500: also strip `status:queued` — an issue staged as "ready"
|
|
616
|
+
# (via `coord queue` / the TUI's "Mark ready") that gets dropped
|
|
617
|
+
# back to Backlog must not carry the marker back in with it; a
|
|
618
|
+
# stale `status:queued` would silently re-surface it as
|
|
619
|
+
# In-progress:ready the moment it's re-tracked into the Pipeline.
|
|
620
|
+
remove_if_present={"status:refining", "status:ready", "status:queued"},
|
|
621
|
+
success_message=f"#{issue} ({slug}) dropped to Backlog",
|
|
622
|
+
no_op_message=f"#{issue} ({slug}) already in Backlog (no status:* label)",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@click.command(
|
|
627
|
+
help=(
|
|
628
|
+
"Stage a Pipeline issue as \"next up\" by tagging it with "
|
|
629
|
+
"`status:queued` — moves a Pipeline:New card into "
|
|
630
|
+
"In-progress:`ready` with no dispatch (display + intent only).\n\n"
|
|
631
|
+
"Deliberately a separate label from `status:ready`: that one is "
|
|
632
|
+
"already applied automatically by `coord track` / the refinement "
|
|
633
|
+
"finalize step for every issue sent to the Pipeline, so it carries "
|
|
634
|
+
"no \"an operator specifically staged this\" signal.\n\n"
|
|
635
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the "
|
|
636
|
+
"GH issue number."
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
@click.argument("repo")
|
|
640
|
+
@click.argument("issue", type=int)
|
|
641
|
+
@_CONFIG_OPTION
|
|
642
|
+
def queue(repo: str, issue: int, config_path: Path) -> None:
|
|
643
|
+
"""#1500: TUI right-click 'Mark ready' fires this command to stage a
|
|
644
|
+
Pipeline:New issue (or an epic + its non-Done children, one call per
|
|
645
|
+
issue) into In-progress:`ready`."""
|
|
646
|
+
cfg = _load_config(config_path)
|
|
647
|
+
repo_entry = cfg.repo(repo)
|
|
648
|
+
slug = repo_entry.github if repo_entry else repo
|
|
649
|
+
_apply_label_change(
|
|
650
|
+
repo, issue, config_path,
|
|
651
|
+
add={"status:queued"},
|
|
652
|
+
remove_if_present=set(),
|
|
653
|
+
success_message=f"#{issue} ({slug}) marked ready (status:queued)",
|
|
654
|
+
no_op_message=f"#{issue} ({slug}) already marked ready (status:queued present)",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@click.command(
|
|
659
|
+
help=(
|
|
660
|
+
"Reverse of `coord queue` — removes `status:queued`, returning an "
|
|
661
|
+
"In-progress:`ready` card to Pipeline:New.\n\n"
|
|
662
|
+
"REPO is the local repo name from coordinator.yml; ISSUE is the "
|
|
663
|
+
"GH issue number."
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
@click.argument("repo")
|
|
667
|
+
@click.argument("issue", type=int)
|
|
668
|
+
@_CONFIG_OPTION
|
|
669
|
+
def unqueue(repo: str, issue: int, config_path: Path) -> None:
|
|
670
|
+
"""#1500: TUI right-click 'Unmark ready' fires this command to strip
|
|
671
|
+
`status:queued`, returning the issue to Pipeline:New."""
|
|
672
|
+
cfg = _load_config(config_path)
|
|
673
|
+
repo_entry = cfg.repo(repo)
|
|
674
|
+
slug = repo_entry.github if repo_entry else repo
|
|
675
|
+
_apply_label_change(
|
|
676
|
+
repo, issue, config_path,
|
|
677
|
+
add=set(),
|
|
678
|
+
remove_if_present={"status:queued"},
|
|
679
|
+
success_message=f"#{issue} ({slug}) unmarked ready (status:queued removed)",
|
|
680
|
+
no_op_message=f"#{issue} ({slug}) not marked ready (no status:queued label)",
|
|
681
|
+
)
|