Skip to content

Commit b7b2d38

Browse files
feat: v3 ticket system — priority/assignee fields, edit command, migration fixes, bridge workflow repair
- Add priority and assignee fields to reducer state schema and CREATE event handling - Add --priority and --assignee args to ticket-create.sh - Add priority/assignee key mappings (pr/asn) to ticket-llm-format.py - Create ticket-edit.sh with EDIT event type for updating fields on existing tickets - Add EDIT to event type allowlist in ticket-lib.sh and reducer - Register edit subcommand in ticket dispatcher - Fix migration script: extract title from body heading, write parent_id/priority/assignee to CREATE events, use original created timestamp, write SYNC events for jira_key, skip tickets with empty titles - Fix inbound/outbound bridge workflows for orphan tickets branch: checkout main for scripts, mount tickets branch as worktree - Remove deprecated tk-plugin files (ticket-query, ticket-edit, tk) - Add comprehensive TDD test coverage for all changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dccbac2 commit b7b2d38

20 files changed

+1483
-6054
lines changed

.github/workflows/inbound-bridge.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,18 @@ jobs:
2626
if: github.actor != (vars.BRIDGE_BOT_LOGIN || 'dso-bridge[bot]')
2727

2828
steps:
29-
- name: Checkout repository
29+
- name: Checkout main (scripts and plugin code)
3030
uses: actions/checkout@v4
3131
with:
3232
fetch-depth: 1
33-
ref: tickets
33+
ref: main
34+
35+
- name: Mount tickets branch as worktree
36+
run: |
37+
git fetch origin tickets --depth=1
38+
# Remove the placeholder .tickets-tracker/ from main (if it exists)
39+
rm -rf .tickets-tracker
40+
git worktree add .tickets-tracker tickets
3441
3542
- name: Set up Python
3643
uses: actions/setup-python@v5
@@ -145,9 +152,10 @@ jobs:
145152

146153
- name: Commit CREATE events back to tickets branch
147154
run: |
148-
# Check if any new CREATE (or BRIDGE_ALERT) events were written
149-
if git diff --quiet HEAD -- .tickets-tracker/; then
150-
echo "No CREATE events to commit — skipping."
155+
cd .tickets-tracker
156+
# Check for any changes (staged, unstaged, or untracked)
157+
if [ -z "$(git status --porcelain)" ]; then
158+
echo "No new events to commit — skipping."
151159
exit 0
152160
fi
153161
@@ -157,7 +165,7 @@ jobs:
157165
git config user.name "${BRIDGE_BOT_NAME}"
158166
git config user.email "${BRIDGE_BOT_EMAIL}"
159167
160-
git add .tickets-tracker/
168+
git add -A
161169
git commit -m "chore: sync CREATE events from Jira inbound bridge [run ${{ github.run_id }}]"
162170
git push origin HEAD:tickets
163171
env:

.github/workflows/outbound-bridge.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
branches:
99
- tickets
1010
paths:
11-
- '.tickets/**'
11+
- '.tickets-tracker/**'
1212

1313
concurrency:
1414
group: jira-bridge
@@ -28,10 +28,17 @@ jobs:
2828
if: github.actor != (vars.BRIDGE_BOT_LOGIN || 'dso-bridge[bot]')
2929

3030
steps:
31-
- name: Checkout repository
31+
- name: Checkout main (scripts and plugin code)
3232
uses: actions/checkout@v4
3333
with:
34-
fetch-depth: 2
34+
fetch-depth: 1
35+
ref: main
36+
37+
- name: Mount tickets branch as worktree
38+
run: |
39+
git fetch origin tickets --depth=2
40+
rm -rf .tickets-tracker
41+
git worktree add .tickets-tracker tickets
3542
3643
- name: Set up Python
3744
uses: actions/setup-python@v5
@@ -133,8 +140,8 @@ jobs:
133140

134141
- name: Commit SYNC events back to tickets branch
135142
run: |
136-
# Check if any new SYNC events were written
137-
if git diff --quiet HEAD -- .tickets-tracker/; then
143+
cd .tickets-tracker
144+
if [ -z "$(git status --porcelain)" ]; then
138145
echo "No SYNC events to commit — skipping."
139146
exit 0
140147
fi
@@ -145,7 +152,7 @@ jobs:
145152
git config user.name "${BRIDGE_BOT_NAME}"
146153
git config user.email "${BRIDGE_BOT_EMAIL}"
147154
148-
git add .tickets-tracker/
155+
git add -A
149156
git commit -m "chore: sync SYNC events from Jira bridge [run ${{ github.run_id }}]"
150157
git push origin HEAD:tickets
151158
env:

plugins/dso/scripts/cutover-tickets-migration.sh

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,24 @@ for line in body_lines:
436436
if note_body:
437437
notes.append(note_body)
438438
439+
# Extract title from first heading in body (not in frontmatter)
440+
title = ""
441+
for line in body_lines:
442+
stripped = line.strip()
443+
if stripped.startswith('# '):
444+
title = stripped[2:].strip()
445+
break
446+
439447
result = {
440448
"id": fm.get("id", ""),
441-
"title": fm.get("title", ""),
449+
"title": title,
442450
"status": fm.get("status", "open"),
443451
"type": fm.get("type", "task"),
444452
"priority": fm.get("priority", "2"),
445453
"parent": fm.get("parent", ""),
454+
"created": fm.get("created", ""),
455+
"assignee": fm.get("assignee", ""),
456+
"jira_key": fm.get("jira_key", ""),
446457
"deps": fm.get("deps", []) if isinstance(fm.get("deps"), list) else [],
447458
"links": fm.get("links", []) if isinstance(fm.get("links"), list) else [],
448459
"notes": notes,
@@ -481,33 +492,78 @@ PYEOF
481492
_ticket_title=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('title',''))" "$_parse_result")
482493
_ticket_status=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('status','open'))" "$_parse_result")
483494

495+
if [[ -z "$_ticket_title" ]]; then
496+
echo "WARN: skipping ticket $_ticket_id (empty title)" >&2
497+
(( _skipped_malformed++ )) || true
498+
continue
499+
fi
500+
501+
local _ticket_parent _ticket_created _ticket_assignee _ticket_jira_key _ticket_priority
502+
_ticket_parent=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('parent',''))" "$_parse_result")
503+
_ticket_created=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('created',''))" "$_parse_result")
504+
_ticket_assignee=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('assignee',''))" "$_parse_result")
505+
_ticket_jira_key=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('jira_key',''))" "$_parse_result")
506+
_ticket_priority=$(python3 -c "import json,sys; d=json.loads(sys.argv[1]); print(d.get('priority',''))" "$_parse_result")
507+
484508
# Create ticket directory and write CREATE event JSON directly
485509
mkdir -p "$_tracker_dir/$_ticket_id"
486-
python3 - "$_tracker_dir/$_ticket_id" "$_ticket_id" "$_ticket_type" "$_ticket_title" "$_ticket_status" "$_parse_result" <<'PYEOF'
510+
python3 - "$_tracker_dir/$_ticket_id" "$_ticket_id" "$_ticket_type" "$_ticket_title" "$_ticket_status" "$_parse_result" "$_ticket_parent" "$_ticket_created" "$_ticket_assignee" "$_ticket_jira_key" "$_ticket_priority" <<'PYEOF'
487511
import json, sys, uuid, time
512+
from datetime import datetime
488513
489514
ticket_dir = sys.argv[1]
490515
ticket_id = sys.argv[2]
491516
ticket_type = sys.argv[3]
492517
title = sys.argv[4]
493518
status = sys.argv[5]
494519
parse_json = sys.argv[6]
520+
parent = sys.argv[7] if len(sys.argv) > 7 else ""
521+
created_str = sys.argv[8] if len(sys.argv) > 8 else ""
522+
assignee = sys.argv[9] if len(sys.argv) > 9 else ""
523+
jira_key = sys.argv[10] if len(sys.argv) > 10 else ""
524+
priority_str = sys.argv[11] if len(sys.argv) > 11 else ""
495525
496526
parsed = json.loads(parse_json)
497527
notes = parsed.get("notes", [])
498528
499-
ts = int(time.time())
529+
# Parse original timestamp
530+
if created_str:
531+
try:
532+
dt = datetime.fromisoformat(created_str.replace('Z', '+00:00'))
533+
ts = int(dt.timestamp())
534+
except (ValueError, OSError):
535+
ts = int(time.time())
536+
else:
537+
ts = int(time.time())
538+
500539
event_uuid = str(uuid.uuid4())
501540
541+
# Parse priority to int if possible
542+
priority = None
543+
if priority_str:
544+
try:
545+
priority = int(priority_str)
546+
except ValueError:
547+
priority = None
548+
549+
# Build CREATE event data
550+
create_data = {
551+
"ticket_type": ticket_type,
552+
"title": title,
553+
}
554+
if parent:
555+
create_data["parent_id"] = parent
556+
if priority is not None:
557+
create_data["priority"] = priority
558+
if assignee:
559+
create_data["assignee"] = assignee
560+
502561
# Write CREATE event
503562
create_event = {
504563
"timestamp": ts,
505564
"uuid": event_uuid,
506565
"event_type": "CREATE",
507-
"data": {
508-
"ticket_type": ticket_type,
509-
"title": title,
510-
}
566+
"data": create_data,
511567
}
512568
create_filename = f"{ts}-{event_uuid}-CREATE.json"
513569
with open(f"{ticket_dir}/{create_filename}", "w", encoding="utf-8") as fh:
@@ -566,6 +622,20 @@ for j, dep_id in enumerate(deps):
566622
link_filename = f"{ts4}-{link_uuid}-LINK.json"
567623
with open(f"{ticket_dir}/{link_filename}", "w", encoding="utf-8") as fh:
568624
json.dump(link_event, fh, ensure_ascii=False)
625+
626+
# Write SYNC event if jira_key is present
627+
if jira_key:
628+
sync_ts = ts + 2 + len(notes) + len(deps) + 1
629+
sync_uuid = str(uuid.uuid4())
630+
sync_event = {
631+
"timestamp": sync_ts,
632+
"uuid": sync_uuid,
633+
"event_type": "SYNC",
634+
"jira_key": jira_key,
635+
}
636+
sync_filename = f"{sync_ts}-{sync_uuid}-SYNC.json"
637+
with open(f"{ticket_dir}/{sync_filename}", "w", encoding="utf-8") as fh:
638+
json.dump(sync_event, fh, ensure_ascii=False)
569639
PYEOF
570640

571641
echo "Migrated: $_ticket_id ($_ticket_type)"

plugins/dso/scripts/ticket

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ _usage() {
3434
echo " link Link two tickets (blocks|depends_on|relates_to)"
3535
echo " unlink Remove a link between two tickets"
3636
echo " deps Show dependency graph for a ticket (<ticket_id>)"
37+
echo " edit Edit ticket fields (--title, --priority, --assignee, --ticket_type)"
3738
echo " bridge-status Show last bridge run status [--format=json]
3839
bridge-fsck Audit bridge mappings (orphans, duplicates, stale SYNCs)"
3940
exit 1
@@ -137,6 +138,10 @@ case "$subcommand" in
137138
fi
138139
exec bash "$SCRIPT_DIR/ticket-bridge-status.sh" "$@"
139140
;;
141+
edit)
142+
_ensure_initialized
143+
exec bash "$SCRIPT_DIR/ticket-edit.sh" "$@"
144+
;;
140145
*)
141146
echo "Error: unknown subcommand '$subcommand'" >&2
142147
_usage

plugins/dso/scripts/ticket-create.sh

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
# plugins/dso/scripts/ticket-create.sh
33
# Create a new ticket with a CREATE event committed to the tickets branch.
44
#
5-
# Usage: ticket-create.sh <ticket_type> <title> [parent_id]
5+
# Usage: ticket-create.sh <ticket_type> <title> [parent_id] [--priority <n>] [--assignee <name>]
66
# ticket_type: one of bug, epic, story, task
77
# title: non-empty string
88
# parent_id: optional parent ticket ID (must exist in .tickets-tracker/)
9+
# --priority: optional priority (0-4; 0=critical, 4=backlog)
10+
# --assignee: optional assignee name (defaults to git config user.name)
911
#
1012
# Outputs the created ticket ID to stdout (only the ID — no other output).
1113
set -euo pipefail
@@ -19,10 +21,12 @@ TRACKER_DIR="$REPO_ROOT/.tickets-tracker"
1921

2022
# ── Usage ─────────────────────────────────────────────────────────────────────
2123
_usage() {
22-
echo "Usage: ticket create <ticket_type> <title> [parent_id]" >&2
24+
echo "Usage: ticket create <ticket_type> <title> [parent_id] [--priority <n>] [--assignee <name>]" >&2
2325
echo " ticket_type: bug | epic | story | task" >&2
2426
echo " title: non-empty string" >&2
2527
echo " parent_id: optional parent ticket ID" >&2
28+
echo " --priority: 0-4 (0=critical, 4=backlog)" >&2
29+
echo " --assignee: assignee name (default: git config user.name)" >&2
2630
exit 1
2731
}
2832

@@ -37,6 +41,8 @@ shift 2
3741

3842
# Parse remaining args: support both positional parent_id and --parent <id>
3943
parent_id=""
44+
priority=""
45+
assignee=""
4046
while [ $# -gt 0 ]; do
4147
case "$1" in
4248
--parent)
@@ -47,6 +53,22 @@ while [ $# -gt 0 ]; do
4753
parent_id="${1#--parent=}"
4854
shift
4955
;;
56+
--priority)
57+
priority="$2"
58+
shift 2
59+
;;
60+
--priority=*)
61+
priority="${1#--priority=}"
62+
shift
63+
;;
64+
--assignee)
65+
assignee="$2"
66+
shift 2
67+
;;
68+
--assignee=*)
69+
assignee="${1#--assignee=}"
70+
shift
71+
;;
5072
*)
5173
# Positional: treat as parent_id (backward-compatible)
5274
parent_id="$1"
@@ -55,6 +77,11 @@ while [ $# -gt 0 ]; do
5577
esac
5678
done
5779

80+
# Default assignee to git user.name if not provided
81+
if [ -z "$assignee" ]; then
82+
assignee=$(git config user.name 2>/dev/null || echo "")
83+
fi
84+
5885
# Validate ticket_type
5986
case "$ticket_type" in
6087
bug|epic|story|task) ;;
@@ -124,22 +151,28 @@ temp_event=$(mktemp "$TRACKER_DIR/.tmp-create-XXXXXX")
124151
python3 -c "
125152
import json, sys
126153
154+
data = {
155+
'ticket_type': sys.argv[5],
156+
'title': sys.argv[6],
157+
'parent_id': sys.argv[7] if sys.argv[7] else ''
158+
}
159+
if sys.argv[8]:
160+
data['priority'] = sys.argv[8]
161+
if sys.argv[9]:
162+
data['assignee'] = sys.argv[9]
163+
127164
event = {
128165
'timestamp': int(sys.argv[1]),
129166
'uuid': sys.argv[2],
130167
'event_type': 'CREATE',
131168
'env_id': sys.argv[3],
132169
'author': sys.argv[4],
133-
'data': {
134-
'ticket_type': sys.argv[5],
135-
'title': sys.argv[6],
136-
'parent_id': sys.argv[7] if sys.argv[7] else ''
137-
}
170+
'data': data
138171
}
139172
140-
with open(sys.argv[8], 'w', encoding='utf-8') as f:
173+
with open(sys.argv[10], 'w', encoding='utf-8') as f:
141174
json.dump(event, f, ensure_ascii=False)
142-
" "$timestamp" "$event_uuid" "$env_id" "$author" "$ticket_type" "$title" "$parent_id" "$temp_event" || {
175+
" "$timestamp" "$event_uuid" "$env_id" "$author" "$ticket_type" "$title" "$parent_id" "$priority" "$assignee" "$temp_event" || {
143176
rm -f "$temp_event"
144177
echo "Error: failed to build CREATE event JSON" >&2
145178
exit 1

plugins/dso/scripts/ticket-edit

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)