Skip to content

Commit 81ea1c3

Browse files
fix: v3 ticket system migration for 8 scripts + bridge project filter
Migrate sprint-next-batch.sh, classify-task.sh, enrich-file-impact.sh, check-acceptance-criteria.sh, issue-quality-check.sh, and merge-to-main.sh to use the v3 event-sourced ticket reducer instead of .tickets/*.md files. Remove v2 .tickets/*.md patterns from merge-to-main.sh conflict resolution (v2 format removed from project). Fix bridge-inbound.py: wire JIRA_PROJECT env var through config dict to fetch_jira_changes() so JQL includes project filter. Previously the project param was never passed, causing all-project sync. Add purge-non-project-tickets.sh utility for cleaning up non-target bridge tickets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22553f4 commit 81ea1c3

14 files changed

+823
-68
lines changed

.github/workflows/inbound-bridge.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ jobs:
149149
INBOUND_OVERLAP_BUFFER_MINUTES: ${{ vars.INBOUND_OVERLAP_BUFFER_MINUTES || '15' }}
150150
INBOUND_STATUS_MAPPING: ${{ vars.INBOUND_STATUS_MAPPING || '{}' }}
151151
INBOUND_TYPE_MAPPING: ${{ vars.INBOUND_TYPE_MAPPING || '{}' }}
152+
JIRA_PROJECT: ${{ vars.JIRA_PROJECT }}
152153

153154
- name: Commit CREATE events back to tickets branch
154155
run: |

.test-index

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ plugins/dso/scripts/classify-task.py:tests/scripts/test-classify-task.sh
2727
plugins/dso/scripts/collect-discoveries.sh:tests/scripts/test-collect-discoveries-plugin-root.sh
2828
plugins/dso/scripts/dedup-tickets.sh:tests/scripts/test-dedup-tickets-sync-state.sh
2929
plugins/dso/scripts/ensure-pre-commit.sh:tests/scripts/test-ensure-precommit-config-paths.sh
30+
plugins/dso/scripts/check-acceptance-criteria.sh:tests/scripts/test_issue_quality_check_file_impact.py
31+
plugins/dso/scripts/enrich-file-impact.sh:tests/scripts/test_issue_quality_check_file_impact.py
3032
plugins/dso/scripts/issue-quality-check.sh:tests/scripts/test_issue_quality_check_file_impact.py
3133
plugins/dso/scripts/merge-ticket-index.py:tests/scripts/test-merge-ticket-index.sh
3234
plugins/dso/scripts/pre-commit-format-fix.sh:tests/scripts/test-precommit-format-fix-config-paths.sh

plugins/dso/scripts/bridge-inbound.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,12 +535,15 @@ def _save_batch_cursor(cursor: int) -> None:
535535
if resume and batch_resume_cursor is not None:
536536
start_at_override = int(batch_resume_cursor)
537537

538+
project = config.get("project") or None
539+
538540
# Fetch changes (may raise CalledProcessError on auth failure)
539541
try:
540542
issues = fetch_jira_changes(
541543
acli_client,
542544
last_pull_ts=last_pull_ts,
543545
overlap_buffer_minutes=overlap_buffer_minutes,
546+
project=project,
544547
on_batch_complete=_save_batch_cursor,
545548
start_at_override=start_at_override,
546549
)
@@ -748,6 +751,7 @@ def _save_batch_cursor(cursor: int) -> None:
748751
jira_url = os.environ.get("JIRA_URL", "")
749752
jira_user = os.environ.get("JIRA_USER", "")
750753
jira_api_token = os.environ.get("JIRA_API_TOKEN", "")
754+
jira_project = os.environ.get("JIRA_PROJECT", "")
751755
bridge_env_id = os.environ.get("BRIDGE_ENV_ID", "")
752756
run_id = os.environ.get("GH_RUN_ID", "")
753757
checkpoint_path_str = os.environ.get("INBOUND_CHECKPOINT_PATH", "")
@@ -791,6 +795,7 @@ def _save_batch_cursor(cursor: int) -> None:
791795
"type_mapping": type_mapping,
792796
"checkpoint_file": str(checkpoint_path) if checkpoint_path is not None else "",
793797
"run_id": run_id,
798+
"project": jira_project,
794799
}
795800

796801
process_inbound(

plugins/dso/scripts/check-acceptance-criteria.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ if [ "$ac_count" -ge 1 ]; then
4141
echo "AC_CHECK: pass ($ac_count criteria lines)"
4242
exit 0
4343
else
44-
echo "AC_CHECK: fail - no ACCEPTANCE CRITERIA section in $ID (use: tk create with --acceptance flag or edit .tickets/$ID.md directly)"
44+
echo "AC_CHECK: fail - no ACCEPTANCE CRITERIA section in $ID (use: tk create with --acceptance flag or add an '## Acceptance Criteria' section with checklist items)"
4545
exit 1
4646
fi

plugins/dso/scripts/classify-task.sh

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,35 @@ task_ids=()
5050

5151
if [ "$1" = "--from-epic" ]; then
5252
epic_id="${2:?Missing epic ID}"
53-
# Use tk ready to list open/in-progress tickets, then filter by parent field
54-
# (tk ready does not support --parent, so we filter via file-based grep)
53+
54+
# Determine whether to use v3 (event-sourced) or v2 (.md files) for parent filtering.
55+
# Priority: explicit TICKETS_DIR env (v2) > explicit TICKETS_TRACKER_DIR env (v3) >
56+
# auto-detect based on .tickets-tracker/ existence.
57+
_use_v3=false
58+
if [ -n "${TICKETS_DIR:-}" ]; then
59+
_use_v3=false
60+
elif [ -n "${TICKETS_TRACKER_DIR:-}" ]; then
61+
_use_v3=true
62+
elif [ -d "$REPO_ROOT/.tickets-tracker" ]; then
63+
_use_v3=true
64+
fi
65+
66+
# Use tk ready to list open/in-progress tickets, then filter by parent field.
67+
# v3: use `ticket show <id>` JSON and check parent_id field.
68+
# v2: use .tickets/$id.md file grep (backward compat).
5569
while IFS= read -r tid; do
5670
[ -n "$tid" ] && task_ids+=("$tid")
5771
done < <(tk ready 2>/dev/null | awk '{print $1}' \
5872
| while read -r id; do
59-
ticket_file="$(git rev-parse --show-toplevel)/.tickets/$id.md"
60-
if [ -f "$ticket_file" ] && grep -q "parent: $epic_id" "$ticket_file" 2>/dev/null; then
61-
echo "$id"
73+
if $_use_v3; then
74+
parent=$(ticket show "$id" 2>/dev/null \
75+
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parent_id',''))" 2>/dev/null || echo "")
76+
[ "$parent" = "$epic_id" ] && echo "$id"
77+
else
78+
ticket_file="$REPO_ROOT/.tickets/$id.md"
79+
if [ -f "$ticket_file" ] && grep -q "parent: $epic_id" "$ticket_file" 2>/dev/null; then
80+
echo "$id"
81+
fi
6282
fi
6383
done || true)
6484

plugins/dso/scripts/enrich-file-impact.sh

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ fi
4646

4747
ID="${args[0]}"
4848

49-
# Load ticket content (|| true prevents set -e from silently exiting on tk failure)
50-
output=$("$TK" show "$ID" 2>/dev/null) || output=""
49+
# Load ticket content. Prefer ticket show (v3-aware), fall back to tk show,
50+
# then fall back to reading .tickets/<id>.md directly (test environments with TICKETS_DIR).
51+
TICKET_CMD="${TICKET_CMD:-$SCRIPT_DIR/ticket}"
52+
if [ -n "${TICKETS_TRACKER_DIR:-}" ] && [ -x "$TICKET_CMD" ]; then
53+
output=$("$TICKET_CMD" show "$ID" 2>/dev/null) || output=""
54+
elif [ -n "${TICKETS_DIR:-}" ] && [ -f "${TICKETS_DIR}/${ID}.md" ]; then
55+
output=$(cat "${TICKETS_DIR}/${ID}.md" 2>/dev/null) || output=""
56+
else
57+
output=$("$TK" show "$ID" 2>/dev/null) || output=""
58+
fi
5159
if [ -z "$output" ]; then
5260
echo "ERROR: Could not load ticket $ID" >&2
5361
exit 1
@@ -175,13 +183,36 @@ if [ -z "$file_impact" ]; then
175183
exit 0
176184
fi
177185

178-
# Append to ticket file
179-
ticket_file="$REPO_ROOT/.tickets/${ID}.md"
180-
if [ ! -f "$ticket_file" ]; then
181-
echo "ERROR: Ticket file not found: $ticket_file" >&2
182-
exit 1
186+
# Append file impact to ticket.
187+
# v3 (event-sourced): auto-detected when .tickets-tracker/ exists or TICKETS_TRACKER_DIR is set.
188+
# v2 (flat-file): falls back to direct .tickets/<id>.md append when TICKETS_DIR is set or
189+
# .tickets-tracker/ does not exist.
190+
_use_v3=false
191+
if [ -n "${TICKETS_TRACKER_DIR:-}" ]; then
192+
_use_v3=true
193+
elif [ -z "${TICKETS_DIR:-}" ] && [ -d "$REPO_ROOT/.tickets-tracker" ]; then
194+
_use_v3=true
183195
fi
184196

185-
# Append the file impact section
186-
printf '\n%s\n' "$file_impact" >> "$ticket_file"
187-
echo "File impact section added to $ID"
197+
if [ "$_use_v3" = true ]; then
198+
# v3: write file impact as a COMMENT event via the ticket CLI
199+
TICKET_CMD="${TICKET_CMD:-$SCRIPT_DIR/ticket}"
200+
if [ ! -x "$TICKET_CMD" ]; then
201+
echo "ERROR: ticket CLI not found at $TICKET_CMD" >&2
202+
exit 1
203+
fi
204+
"$TICKET_CMD" comment "$ID" "$file_impact" || {
205+
echo "ERROR: Failed to record file impact comment on ticket $ID" >&2
206+
exit 1
207+
}
208+
echo "File impact section added to $ID (v3 comment event)"
209+
else
210+
# v2: append directly to the markdown file
211+
ticket_file="${TICKETS_DIR:-$REPO_ROOT/.tickets}/${ID}.md"
212+
if [ ! -f "$ticket_file" ]; then
213+
echo "ERROR: Ticket file not found: $ticket_file" >&2
214+
exit 1
215+
fi
216+
printf '\n%s\n' "$file_impact" >> "$ticket_file"
217+
echo "File impact section added to $ID"
218+
fi

plugins/dso/scripts/issue-quality-check.sh

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@ fi
2929
ID="$1"
3030

3131
# Get the full issue output (stays in script, not in orchestrator context).
32-
# tk show exits 0 even when an issue is not found (error goes to stderr).
33-
# Detect failure by checking for an empty output after suppressing stderr.
34-
output=$("$TK" show "$ID" 2>/dev/null)
32+
# Prefer ticket show (v3-aware), fall back to tk show, then fall back to
33+
# reading .tickets/<id>.md directly (test environments with TICKETS_DIR).
34+
TICKET_CMD="${TICKET_CMD:-$SCRIPT_DIR/ticket}"
35+
if [ -n "${TICKETS_TRACKER_DIR:-}" ] && [ -x "$TICKET_CMD" ]; then
36+
output=$("$TICKET_CMD" show "$ID" 2>/dev/null) || output=""
37+
elif [ -n "${TICKETS_DIR:-}" ] && [ -f "${TICKETS_DIR}/${ID}.md" ]; then
38+
output=$(cat "${TICKETS_DIR}/${ID}.md" 2>/dev/null) || output=""
39+
else
40+
output=$("$TK" show "$ID" 2>/dev/null) || output=""
41+
fi
3542
if [ -z "$output" ]; then
3643
echo "QUALITY: fail - could not load issue $ID, using inline prompt"
3744
exit 1

plugins/dso/scripts/merge-to-main.sh

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,12 @@ _abort_stale_rebase() {
413413
# accepting the archive move: git rm the old path (if still present) and git add the
414414
# archived path (if present). Non-archive conflicts cause an immediate abort.
415415
#
416+
# v3 ticket system (.tickets-tracker/): ticket event JSON files and the index
417+
# (.tickets-tracker/<id>/*.json, .tickets-tracker/.index.json) are managed on a
418+
# separate orphan branch and are excluded from the main repo's tracked files.
419+
# However, if they appear as conflicts during rebase (e.g., during worktree sync),
420+
# they are always safe ticket-data files and can be auto-resolved by accepting ours.
421+
#
416422
# Usage: call from the git pull --rebase failure handler in _phase_sync.
417423
# Must be called while a rebase is in progress (REBASE_HEAD exists).
418424
# Returns 0 on success (rebase continued), 1 on failure (rebase aborted).
@@ -452,15 +458,14 @@ _auto_resolve_archive_conflicts() {
452458
return 1
453459
fi
454460

455-
# Safety check: ALL conflicts must be archive-type ticket files.
456-
# Archive-type = either .tickets/archive/xxx.md or .tickets/xxx.md being
457-
# deleted in favor of .tickets/archive/xxx.md.
461+
# Safety check: ALL conflicts must be ticket-data files (safe to auto-resolve).
462+
# Ticket data lives in .tickets-tracker/<id>/*.json event files or .tickets-tracker/.index.json.
458463
local _non_archive_conflicts=0
459464
while IFS= read -r _file; do
460465
[[ -z "$_file" ]] && continue
461466
case "$_file" in
462-
.tickets/archive/*.md | .tickets/*.md)
463-
# These are archive-type ticket paths — safe to auto-resolve
467+
.tickets-tracker/*.json | .tickets-tracker/*/*.json)
468+
# v3: ticket event JSON files or index — safe to auto-resolve
464469
;;
465470
*)
466471
_non_archive_conflicts=$(( _non_archive_conflicts + 1 ))
@@ -474,30 +479,20 @@ _auto_resolve_archive_conflicts() {
474479
return 1
475480
fi
476481

477-
# All conflicts are archive-type — resolve each one.
478-
# For rename/delete: accept the deletion (git rm old path) and
479-
# accept the addition of the archive path (git add archive path, if it exists).
482+
# All conflicts are ticket-data files — resolve each one.
483+
# For JSON event file conflicts: accept ours (git add) — event files are
484+
# append-only and our version is always the authoritative local state.
480485
local _resolved=0
481486
local _failed=0
482487

483488
while IFS= read -r _file; do
484489
[[ -z "$_file" ]] && continue
485490

486-
# Determine if this is the old path or the archive path
487-
if [[ "$_file" == .tickets/archive/*.md ]]; then
488-
# This is the archive destination — add it if it exists in working tree
491+
if [[ "$_file" == .tickets-tracker/*.json || "$_file" == .tickets-tracker/*/*.json ]]; then
492+
# v3: Ticket event JSON files / index — accept ours (git add if present, git rm if absent)
489493
if [[ -f "$_file" ]]; then
490494
git add "$_file" 2>/dev/null && _resolved=$(( _resolved + 1 )) || _failed=$(( _failed + 1 ))
491495
else
492-
# File doesn't exist in working tree — this side deleted it; skip
493-
_resolved=$(( _resolved + 1 ))
494-
fi
495-
elif [[ "$_file" == .tickets/*.md ]]; then
496-
# This is the old (pre-archive) path — remove it if still present
497-
if [[ -f "$_file" ]]; then
498-
git rm --force --quiet "$_file" 2>/dev/null && _resolved=$(( _resolved + 1 )) || _failed=$(( _failed + 1 ))
499-
else
500-
# Already gone — mark as resolved
501496
git rm --quiet --cached "$_file" 2>/dev/null || true
502497
_resolved=$(( _resolved + 1 ))
503498
fi
@@ -558,35 +553,30 @@ _auto_resolve_archive_conflicts() {
558553
continue
559554
fi
560555

561-
# Validate that all new conflicts are still archive-type.
556+
# Validate that all new conflicts are still ticket-data files.
562557
local _new_non_archive=0
563558
while IFS= read -r _nf; do
564559
[[ -z "$_nf" ]] && continue
565560
case "$_nf" in
566-
.tickets/archive/*.md | .tickets/*.md) ;;
561+
.tickets-tracker/*.json | .tickets-tracker/*/*.json) ;;
567562
*) _new_non_archive=$(( _new_non_archive + 1 )) ;;
568563
esac
569564
done <<< "$_new_all"
570565

571566
if [[ "$_new_non_archive" -gt 0 ]]; then
572-
echo "INFO: _auto_resolve_archive_conflicts: non-archive conflicts in subsequent commit — aborting auto-resolve." >&2
567+
echo "INFO: _auto_resolve_archive_conflicts: non-ticket conflicts in subsequent commit — aborting auto-resolve." >&2
573568
git rebase --abort 2>/dev/null || true
574569
return 1
575570
fi
576571

577-
# Resolve the new archive conflicts.
572+
# Resolve the new ticket-data conflicts (v3 JSON event files).
578573
local _new_resolved=0 _new_failed=0
579574
while IFS= read -r _nf; do
580575
[[ -z "$_nf" ]] && continue
581-
if [[ "$_nf" == .tickets/archive/*.md ]]; then
576+
if [[ "$_nf" == .tickets-tracker/*.json || "$_nf" == .tickets-tracker/*/*.json ]]; then
577+
# v3: ticket event JSON / index — accept ours
582578
if [[ -f "$_nf" ]]; then
583579
git add "$_nf" 2>/dev/null && _new_resolved=$(( _new_resolved + 1 )) || _new_failed=$(( _new_failed + 1 ))
584-
else
585-
_new_resolved=$(( _new_resolved + 1 ))
586-
fi
587-
elif [[ "$_nf" == .tickets/*.md ]]; then
588-
if [[ -f "$_nf" ]]; then
589-
git rm --force --quiet "$_nf" 2>/dev/null && _new_resolved=$(( _new_resolved + 1 )) || _new_failed=$(( _new_failed + 1 ))
590580
else
591581
git rm --quiet --cached "$_nf" 2>/dev/null || true
592582
_new_resolved=$(( _new_resolved + 1 ))
@@ -1015,11 +1005,11 @@ _phase_sync() {
10151005
fi
10161006
_abort_stale_rebase
10171007
if ! git pull --rebase 2>&1; then
1018-
# Attempt auto-resolution of archive rename/delete conflicts before giving up.
1019-
# Archive conflicts occur when a previous merge archived tickets and the remote
1020-
# has a different archive commit — always safe to resolve automatically.
1008+
# Attempt auto-resolution of ticket-data conflicts before giving up.
1009+
# Ticket-data conflicts (v2 archive rename/delete or v3 JSON event files)
1010+
# are always safe to resolve automatically.
10211011
if _auto_resolve_archive_conflicts 2>&1; then
1022-
echo "OK: Archive rename/delete conflicts auto-resolved during pull --rebase."
1012+
echo "OK: Ticket-data conflicts auto-resolved during pull --rebase."
10231013
else
10241014
if $STASHED; then git stash pop --quiet 2>/dev/null || true; fi
10251015
_set_phase_status "pull_rebase" "conflict"
@@ -1033,8 +1023,8 @@ _phase_sync() {
10331023
echo "Restoring stashed changes..."
10341024
if ! git stash pop --quiet 2>/dev/null; then
10351025
# Stash pop conflicted — discard the stash. The pre-stash files were
1036-
# .tickets/ files, which the merge step will overwrite
1037-
# anyway. Keeping an unmerged stash pop would block all subsequent ops.
1026+
# ticket data files (.tickets/ or .tickets-tracker/), which the merge
1027+
# step will overwrite anyway. Keeping an unmerged stash pop would block all subsequent ops.
10381028
echo "WARNING: Stash pop had conflicts — resetting. Merge step will reconcile."
10391029
git reset --merge 2>/dev/null || true
10401030
git stash drop --quiet 2>/dev/null || true
@@ -1067,7 +1057,7 @@ _phase_merge() {
10671057
MERGE_MSG="$LAST_MSG (merge $BRANCH)"
10681058

10691059
# Capture pre-merge SHA so we can later detect whether the merge contained
1070-
# non-.tickets/ changes (used for the CI trigger check after push).
1060+
# non-ticket-data changes (used for the CI trigger check after push).
10711061
PRE_MERGE_SHA=$(git rev-parse HEAD)
10721062

10731063
echo "Merging $BRANCH into main..."
@@ -1162,9 +1152,10 @@ _phase_validate() {
11621152
# Stage any post-merge artifacts (.gitignore entries for worktree dirs)
11631153
git add .gitignore 2>/dev/null || true
11641154

1165-
# Auto-stage .tickets/ changes — CI failure tracking pushes ticket commits directly
1166-
# to main, and the merge can leave .tickets/ files dirty. These are data files, not
1167-
# code, so auto-staging into the merge commit is safe.
1155+
# Auto-stage ticket-data changes ($TICKETS_DIR/) — CI failure tracking pushes ticket
1156+
# commits directly to main, and the merge can leave ticket data files dirty.
1157+
# These are data files, not code, so auto-staging into the merge commit is safe.
1158+
# (v2: .tickets/*.md files; v3: $TICKETS_DIR/ may be absent if fully on .tickets-tracker/ branch)
11681159
git add "$TICKETS_DIR"/ 2>/dev/null || true
11691160

11701161
# Check for dirty tracked files (modified but not staged) excluding tickets dir

0 commit comments

Comments
 (0)