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>
179 lines
6.2 KiB
Python
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
|