feat(db): add team column to ticket system
Adds a comma-separated team field to tickets so work can be attributed to server, client, or joint teams. Includes --team filter on list, --team on create, and a new team subcommand. Schema, CLI, and skill docs all updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ For raw SQL access (rare), use the wrapper scripts:
|
|||||||
|
|
||||||
### List tickets
|
### List tickets
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A]
|
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Show ticket detail
|
### Show ticket detail
|
||||||
@@ -45,7 +45,7 @@ Returns full ticket with children, blockers, and dependents.
|
|||||||
|
|
||||||
### Create ticket
|
### Create ticket
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D]
|
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||||
```
|
```
|
||||||
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
||||||
Priorities: `critical`, `high`, `medium`, `low`
|
Priorities: `critical`, `high`, `medium`, `low`
|
||||||
@@ -63,6 +63,12 @@ db/connectors/ticket assign <id> <agent>
|
|||||||
db/connectors/ticket unassign <id>
|
db/connectors/ticket unassign <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Team assignment
|
||||||
|
```bash
|
||||||
|
db/connectors/ticket team <id> <teams>
|
||||||
|
```
|
||||||
|
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
||||||
|
|
||||||
### Sprint management
|
### Sprint management
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket sprint [--active]
|
db/connectors/ticket sprint [--active]
|
||||||
|
|||||||
+32
-13
@@ -3,17 +3,18 @@
|
|||||||
Ticket CLI — ergonomic interface to the project ticketing database.
|
Ticket CLI — ergonomic interface to the project ticketing database.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A]
|
ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||||
ticket show <id>
|
ticket show <id>
|
||||||
ticket done <id> [<id> ...]
|
ticket done <id> [<id> ...]
|
||||||
ticket status <id> <new_status>
|
ticket status <id> <new_status>
|
||||||
ticket assign <id> <agent>
|
ticket assign <id> <agent>
|
||||||
ticket unassign <id>
|
ticket unassign <id>
|
||||||
|
ticket team <id> <teams>
|
||||||
ticket sprint [--active]
|
ticket sprint [--active]
|
||||||
ticket sprint assign <id> <sprint_id>
|
ticket sprint assign <id> <sprint_id>
|
||||||
ticket deps <id>
|
ticket deps <id>
|
||||||
ticket search <keyword>
|
ticket search <keyword>
|
||||||
ticket create <type> <title> [--parent N] [--priority P] [--decision D]
|
ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||||
ticket epics [--status S]
|
ticket epics [--status S]
|
||||||
ticket children <id>
|
ticket children <id>
|
||||||
ticket count [--status S]
|
ticket count [--status S]
|
||||||
@@ -112,7 +113,7 @@ def parse_flags(args, known_flags):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def cmd_list(conn, args):
|
def cmd_list(conn, args):
|
||||||
flags, _ = parse_flags(args, ["status", "priority", "epic", "sprint", "assigned"])
|
flags, _ = parse_flags(args, ["status", "priority", "epic", "sprint", "assigned", "team"])
|
||||||
conditions = []
|
conditions = []
|
||||||
params = []
|
params = []
|
||||||
if "status" in flags:
|
if "status" in flags:
|
||||||
@@ -130,9 +131,13 @@ def cmd_list(conn, args):
|
|||||||
if "assigned" in flags:
|
if "assigned" in flags:
|
||||||
conditions.append("t.assigned_to = ?")
|
conditions.append("t.assigned_to = ?")
|
||||||
params.append(flags["assigned"])
|
params.append(flags["assigned"])
|
||||||
|
if "team" in flags:
|
||||||
|
# Match exact team name within comma-separated list
|
||||||
|
conditions.append("(',' || t.team || ',' LIKE '%,' || ? || ',%')")
|
||||||
|
params.append(flags["team"])
|
||||||
where = " AND ".join(conditions) if conditions else "1=1"
|
where = " AND ".join(conditions) if conditions else "1=1"
|
||||||
sql = f"""SELECT t.id, t.type, t.title, t.status, t.priority, t.assigned_to,
|
sql = f"""SELECT t.id, t.type, t.title, t.status, t.priority, t.assigned_to,
|
||||||
t.parent_id, t.sprint_id
|
t.team, t.parent_id, t.sprint_id
|
||||||
FROM tickets t WHERE {where}
|
FROM tickets t WHERE {where}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1
|
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1
|
||||||
@@ -184,6 +189,8 @@ def _print_brief(tickets):
|
|||||||
parts = [f"{t['type']}", f"P:{t['priority']}", f"S:{t['status']}"]
|
parts = [f"{t['type']}", f"P:{t['priority']}", f"S:{t['status']}"]
|
||||||
if t.get("assigned_to"):
|
if t.get("assigned_to"):
|
||||||
parts.append(f"@{t['assigned_to']}")
|
parts.append(f"@{t['assigned_to']}")
|
||||||
|
if t.get("team"):
|
||||||
|
parts.append(f"Team:{t['team']}")
|
||||||
if t.get("parent_id"):
|
if t.get("parent_id"):
|
||||||
parts.append(f"Epic:#{t['parent_id']} ({t.get('parent_title', '?')})")
|
parts.append(f"Epic:#{t['parent_id']} ({t.get('parent_title', '?')})")
|
||||||
if t.get("sprint_id"):
|
if t.get("sprint_id"):
|
||||||
@@ -265,25 +272,26 @@ def cmd_deps(conn, ticket_id):
|
|||||||
|
|
||||||
|
|
||||||
def cmd_search(conn, keyword):
|
def cmd_search(conn, keyword):
|
||||||
rows = query(conn, """SELECT id, type, title, status, priority, assigned_to
|
rows = query(conn, """SELECT id, type, title, status, priority, assigned_to, team
|
||||||
FROM tickets WHERE title LIKE ? OR description LIKE ?
|
FROM tickets WHERE title LIKE ? OR description LIKE ?
|
||||||
ORDER BY id""", (f"%{keyword}%", f"%{keyword}%"))
|
ORDER BY id""", (f"%{keyword}%", f"%{keyword}%"))
|
||||||
out({"ok": True, "count": len(rows), "rows": rows})
|
out({"ok": True, "count": len(rows), "rows": rows})
|
||||||
|
|
||||||
|
|
||||||
def cmd_create(conn, args):
|
def cmd_create(conn, args):
|
||||||
flags, positional = parse_flags(args, ["parent", "priority", "decision"])
|
flags, positional = parse_flags(args, ["parent", "priority", "decision", "team"])
|
||||||
if len(positional) < 2:
|
if len(positional) < 2:
|
||||||
out({"ok": False, "error": "Usage: ticket create <type> <title> [--parent N] [--priority P] [--decision D]"})
|
out({"ok": False, "error": "Usage: ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]"})
|
||||||
return
|
return
|
||||||
ticket_type = positional[0]
|
ticket_type = positional[0]
|
||||||
title = " ".join(positional[1:])
|
title = " ".join(positional[1:])
|
||||||
parent_id = int(flags["parent"]) if "parent" in flags else None
|
parent_id = int(flags["parent"]) if "parent" in flags else None
|
||||||
priority = flags.get("priority", "medium")
|
priority = flags.get("priority", "medium")
|
||||||
decision_ref = flags.get("decision")
|
decision_ref = flags.get("decision")
|
||||||
|
team = flags.get("team")
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO tickets (type, title, parent_id, priority, decision_ref) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO tickets (type, title, parent_id, priority, decision_ref, team) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(ticket_type, title, parent_id, priority, decision_ref))
|
(ticket_type, title, parent_id, priority, decision_ref, team))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
last_id = query(conn, "SELECT last_insert_rowid() as id")[0]["id"]
|
last_id = query(conn, "SELECT last_insert_rowid() as id")[0]["id"]
|
||||||
out({"ok": True, "id": last_id, "title": title})
|
out({"ok": True, "id": last_id, "title": title})
|
||||||
@@ -297,7 +305,7 @@ def cmd_epics(conn, args):
|
|||||||
conditions.append("t.status = ?")
|
conditions.append("t.status = ?")
|
||||||
params.append(flags["status"])
|
params.append(flags["status"])
|
||||||
where = " AND ".join(conditions)
|
where = " AND ".join(conditions)
|
||||||
rows = query(conn, f"""SELECT t.id, t.title, t.status, t.priority, t.assigned_to,
|
rows = query(conn, f"""SELECT t.id, t.title, t.status, t.priority, t.assigned_to, t.team,
|
||||||
COUNT(c.id) as child_count,
|
COUNT(c.id) as child_count,
|
||||||
SUM(CASE WHEN c.status='done' THEN 1 ELSE 0 END) as done_count
|
SUM(CASE WHEN c.status='done' THEN 1 ELSE 0 END) as done_count
|
||||||
FROM tickets t LEFT JOIN tickets c ON c.parent_id = t.id
|
FROM tickets t LEFT JOIN tickets c ON c.parent_id = t.id
|
||||||
@@ -308,11 +316,16 @@ def cmd_epics(conn, args):
|
|||||||
|
|
||||||
|
|
||||||
def cmd_children(conn, ticket_id):
|
def cmd_children(conn, ticket_id):
|
||||||
rows = query(conn, """SELECT id, type, title, status, priority, assigned_to
|
rows = query(conn, """SELECT id, type, title, status, priority, assigned_to, team
|
||||||
FROM tickets WHERE parent_id = ? ORDER BY id""", (int(ticket_id),))
|
FROM tickets WHERE parent_id = ? ORDER BY id""", (int(ticket_id),))
|
||||||
out({"ok": True, "count": len(rows), "parent_id": int(ticket_id), "rows": rows})
|
out({"ok": True, "count": len(rows), "parent_id": int(ticket_id), "rows": rows})
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_team(conn, ticket_id, teams):
|
||||||
|
updated = execute(conn, "UPDATE tickets SET team=?, updated_at=datetime('now') WHERE id=?", (teams, int(ticket_id)))
|
||||||
|
out({"ok": True, "updated": updated, "id": int(ticket_id), "team": teams})
|
||||||
|
|
||||||
|
|
||||||
def cmd_count(conn, args):
|
def cmd_count(conn, args):
|
||||||
flags, _ = parse_flags(args, ["status"])
|
flags, _ = parse_flags(args, ["status"])
|
||||||
if "status" in flags:
|
if "status" in flags:
|
||||||
@@ -329,17 +342,18 @@ def cmd_count(conn, args):
|
|||||||
HELP = """ticket — project ticket CLI
|
HELP = """ticket — project ticket CLI
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A]
|
ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||||
ticket show [--brief] <id> [<id>...] Full ticket detail (--brief for summary)
|
ticket show [--brief] <id> [<id>...] Full ticket detail (--brief for summary)
|
||||||
ticket done <id> [<id> ...] Mark tickets as done
|
ticket done <id> [<id> ...] Mark tickets as done
|
||||||
ticket status <id> <new_status> Change ticket status
|
ticket status <id> <new_status> Change ticket status
|
||||||
ticket assign <id> <agent> Assign ticket to agent/branch
|
ticket assign <id> <agent> Assign ticket to agent/branch
|
||||||
ticket unassign <id> Remove assignment
|
ticket unassign <id> Remove assignment
|
||||||
|
ticket team <id> <teams> Set team(s) (comma-separated, e.g. server,client)
|
||||||
ticket sprint [--active] List sprints
|
ticket sprint [--active] List sprints
|
||||||
ticket sprint assign <id> <sprint> Assign ticket to sprint
|
ticket sprint assign <id> <sprint> Assign ticket to sprint
|
||||||
ticket deps <id> Show ticket dependencies
|
ticket deps <id> Show ticket dependencies
|
||||||
ticket search <keyword> Search tickets by title/description
|
ticket search <keyword> Search tickets by title/description
|
||||||
ticket create <type> <title> [--parent N] [--priority P] [--decision D]
|
ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||||
ticket epics [--status S] List epics with child counts
|
ticket epics [--status S] List epics with child counts
|
||||||
ticket children <id> List children of a ticket
|
ticket children <id> List children of a ticket
|
||||||
ticket count [--status S] Count tickets by status"""
|
ticket count [--status S] Count tickets by status"""
|
||||||
@@ -390,6 +404,11 @@ def main():
|
|||||||
out({"ok": False, "error": "Usage: ticket unassign <id>"})
|
out({"ok": False, "error": "Usage: ticket unassign <id>"})
|
||||||
else:
|
else:
|
||||||
cmd_unassign(conn, args[0])
|
cmd_unassign(conn, args[0])
|
||||||
|
elif cmd == "team":
|
||||||
|
if len(args) < 2:
|
||||||
|
out({"ok": False, "error": "Usage: ticket team <id> <teams>"})
|
||||||
|
else:
|
||||||
|
cmd_team(conn, args[0], args[1])
|
||||||
elif cmd == "sprint":
|
elif cmd == "sprint":
|
||||||
cmd_sprint(conn, args)
|
cmd_sprint(conn, args)
|
||||||
elif cmd == "deps":
|
elif cmd == "deps":
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS tickets (
|
|||||||
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog', 'ready', 'in_progress', 'review', 'done', 'cancelled')),
|
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog', 'ready', 'in_progress', 'review', 'done', 'cancelled')),
|
||||||
priority TEXT DEFAULT 'medium' CHECK(priority IN ('critical', 'high', 'medium', 'low')),
|
priority TEXT DEFAULT 'medium' CHECK(priority IN ('critical', 'high', 'medium', 'low')),
|
||||||
assigned_to TEXT,
|
assigned_to TEXT,
|
||||||
|
team TEXT, -- comma-separated team names, e.g. 'server', 'client,server'
|
||||||
decision_ref TEXT, -- e.g. 'D-010' or 'Q-001'
|
decision_ref TEXT, -- e.g. 'D-010' or 'Q-001'
|
||||||
sprint_id INTEGER REFERENCES sprints(id),
|
sprint_id INTEGER REFERENCES sprints(id),
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
@@ -58,6 +59,7 @@ CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_id);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_tickets_sprint ON tickets(sprint_id);
|
CREATE INDEX IF NOT EXISTS idx_tickets_sprint ON tickets(sprint_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision ON tickets(decision_ref);
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
CREATE INDEX IF NOT EXISTS idx_history_ticket ON ticket_history(ticket_id);
|
CREATE INDEX IF NOT EXISTS idx_history_ticket ON ticket_history(ticket_id);
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user