Files
clide/tools/scripts/planning/cmd_ticket.py
T
jpmschweitzerandClaude a96a54ec7f add Python stopgap planning tooling under tools/scripts/plan
Ports settled-reach's decisions_sync.py + ticket + decision scripts,
Scrum-stripped. Writes to .pql/pql.db (gitignored). Verb shape
mirrors the eventual `pql` subcommands so migration is a call-site
find-replace once pql ships feature parity (D-040, R-011).

Ticket IDs are T-NNN (TEXT PKs) — reshape from settled-reach's
integer auto-increment so the stopgap's writes are compatible with
pql's future reads without a data migration. No `sprints` table; the
pyramid is kanban / waterfall (D-035).

Verified end-to-end: sync parses 71 records (39 confirmed, 23 open
questions, 9 rejected) with 62 cross-refs and zero broken links;
`ticket new`, `ticket status`, `ticket board`, and `decisions show
--with-refs` all round-trip correctly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-21 17:28:10 +02:00

179 lines
6.2 KiB
Python

"""`plan ticket …` subcommands."""
from __future__ import annotations
import argparse
import sys
from . import db, repo_tickets
from .format import emit, emit_board, rows_to_dicts
def add_subparsers(parent: argparse._SubParsersAction) -> None:
p = parent.add_parser("ticket", help="ticket subcommands")
sub = p.add_subparsers(dest="subcmd", required=True)
new = sub.add_parser("new", help="create a ticket")
new.add_argument("type", choices=["initiative", "epic", "story", "task", "bug"])
new.add_argument("title")
new.add_argument("--parent")
new.add_argument("--priority", choices=["critical", "high", "medium", "low"], default="medium")
new.add_argument("--decision", dest="decision_ref")
new.add_argument("--team")
new.add_argument("--description")
ls = sub.add_parser("list", help="list tickets")
ls.add_argument("--status")
ls.add_argument("--team")
ls.add_argument("--assigned")
ls.add_argument("--decision")
ls.add_argument("--label")
ls.add_argument("--output", choices=["table", "json"])
show = sub.add_parser("show", help="show a ticket")
show.add_argument("id")
show.add_argument("--with-decision", action="store_true")
show.add_argument("--with-blockers", action="store_true")
show.add_argument("--with-children", action="store_true")
show.add_argument("--output", choices=["table", "json"])
st = sub.add_parser("status", help="change status")
st.add_argument("id")
st.add_argument("new_status", choices=[
"backlog", "ready", "in_progress", "review", "done", "cancelled",
])
asg = sub.add_parser("assign", help="assign to agent")
asg.add_argument("id")
asg.add_argument("agent")
tm = sub.add_parser("team", help="set team")
tm.add_argument("id")
tm.add_argument("team")
blk = sub.add_parser("block", help="mark <id> as blocked by <other>")
blk.add_argument("id")
blk.add_argument("--by", required=True, dest="by")
ublk = sub.add_parser("unblock", help="remove a blocker")
ublk.add_argument("id")
ublk.add_argument("--from", required=True, dest="from_")
lbl = sub.add_parser("label", help="add/rm labels")
lbl.add_argument("id")
lbl.add_argument("op", choices=["add", "rm"])
lbl.add_argument("label")
srch = sub.add_parser("search", help="search title + description")
srch.add_argument("query")
srch.add_argument("--output", choices=["table", "json"])
brd = sub.add_parser("board", help="kanban columns")
brd.add_argument("--team")
brd.add_argument("--output", choices=["table", "json"])
def dispatch(args: argparse.Namespace) -> int:
conn = db.connect()
try:
if args.subcmd == "new":
tid = repo_tickets.create(
conn,
type_=args.type,
title=args.title,
description=args.description,
parent_id=args.parent,
priority=args.priority,
decision_ref=args.decision_ref,
team=args.team,
)
print(tid)
return 0
if args.subcmd == "list":
rows = repo_tickets.list_(
conn,
status=args.status,
team=args.team,
assigned=args.assigned,
decision=args.decision,
label=args.label,
)
emit(rows_to_dicts(rows), mode=args.output)
return 0
if args.subcmd == "show":
row = repo_tickets.get(conn, args.id)
if row is None:
print(f"error: {args.id} not found", file=sys.stderr)
return 3
data: dict = dict(row)
if args.with_decision and row["decision_ref"]:
dec = conn.execute(
"SELECT * FROM decisions WHERE id = ?", (row["decision_ref"],)
).fetchone()
data["decision"] = dict(dec) if dec else None
if args.with_blockers:
data["blocked_by"] = rows_to_dicts(repo_tickets.blockers(conn, args.id))
if args.with_children:
data["children"] = rows_to_dicts(repo_tickets.children(conn, args.id))
emit(data, mode=args.output)
return 0
if args.subcmd == "status":
n = repo_tickets.set_status(conn, args.id, args.new_status)
if n == 0:
print(f"error: {args.id} not found", file=sys.stderr)
return 3
print(f"{args.id}{args.new_status}")
return 0
if args.subcmd == "assign":
n = repo_tickets.set_field(conn, args.id, "assigned_to", args.agent)
if n == 0:
print(f"error: {args.id} not found", file=sys.stderr)
return 3
print(f"{args.id} assigned to {args.agent}")
return 0
if args.subcmd == "team":
n = repo_tickets.set_field(conn, args.id, "team", args.team)
if n == 0:
print(f"error: {args.id} not found", file=sys.stderr)
return 3
print(f"{args.id} team = {args.team}")
return 0
if args.subcmd == "block":
repo_tickets.block(conn, args.id, args.by)
print(f"{args.id} blocked by {args.by}")
return 0
if args.subcmd == "unblock":
n = repo_tickets.unblock(conn, args.id, args.from_)
if n == 0:
print(f"(no such blocker)")
else:
print(f"{args.id} unblocked from {args.from_}")
return 0
if args.subcmd == "label":
if args.op == "add":
repo_tickets.label_add(conn, args.id, args.label)
else:
repo_tickets.label_rm(conn, args.id, args.label)
print(f"{args.id} labels {args.op} {args.label}")
return 0
if args.subcmd == "search":
rows = repo_tickets.search(conn, args.query)
emit(rows_to_dicts(rows), mode=args.output)
return 0
if args.subcmd == "board":
columns = repo_tickets.board(conn, team=args.team)
emit_board(columns, mode=args.output)
return 0
finally:
conn.close()
return 1