@@ -35,11 +35,26 @@ REDUCER="$SCRIPT_DIR/ticket-reducer.py"
3535TRACKER_DIR=" ${TICKETS_TRACKER_DIR:- $REPO_ROOT / .tickets-tracker} "
3636
3737# ---------------------------------------------------------------------------
38- # Build index from v3 reducer.
38+ # Retry configuration for worktree startup race conditions.
39+ # When the tracker dir has entries but the reducer returns an empty index,
40+ # retry after a short wait. This handles the case where the tracker symlink
41+ # or filesystem isn't fully ready yet (common during worktree creation).
42+ # ---------------------------------------------------------------------------
43+ # REVIEW-DEFENSE: MAX_RETRIES is the number of additional attempts after the initial build,
44+ # not the total number of attempts. Total attempts = MAX_RETRIES + 1 (initial attempt + retries).
45+ # The retry loop condition `attempt < MAX_RETRIES` is intentional: attempt starts at 0 and
46+ # increments after each retry, so the loop runs at most MAX_RETRIES times (additional attempts).
47+ MAX_RETRIES=" ${SPRINT_MAX_RETRIES:- 3} "
48+ RETRY_WAIT=" ${SPRINT_RETRY_WAIT:- 1} "
49+
50+ # ---------------------------------------------------------------------------
51+ # Build index from v3 reducer (with retry on transient failure).
3952# ---------------------------------------------------------------------------
4053export _SPRINT_TRACKER_DIR=" $TRACKER_DIR "
4154export _SPRINT_REDUCER=" $REDUCER "
42- index_and_counts=$( python3 -c "
55+
56+ _build_index () {
57+ python3 -c "
4358import json, os, sys, importlib.util, collections
4459
4560tracker_dir = os.environ['_SPRINT_TRACKER_DIR']
@@ -90,7 +105,38 @@ for entry_name in os.listdir(tracker_dir):
90105 child_counts[parent_id] += 1
91106
92107print(json.dumps({'index': idx, 'child_counts': dict(child_counts)}))
93- " 2> /dev/null || echo ' {"index":{},"child_counts":{}}' )
108+ " 2> /dev/null || echo ' {"index":{},"child_counts":{}}'
109+ }
110+
111+ # Count non-hidden subdirectories in tracker to detect "has entries but reducer failed"
112+ _tracker_has_entries () {
113+ local count
114+ count=$( find " $TRACKER_DIR " -mindepth 1 -maxdepth 1 -type d ! -name ' .*' 2> /dev/null | head -1)
115+ [ -n " $count " ]
116+ }
117+
118+ # Build index with retry on transient failure
119+ index_and_counts=$( _build_index)
120+
121+ attempt=0
122+ while [ " $attempt " -lt " $MAX_RETRIES " ]; do
123+ # Check if the index is empty (no tickets resolved)
124+ index_key_count=$( echo " $index_and_counts " | python3 -c " import json,sys; print(len(json.load(sys.stdin).get('index',{})))" 2> /dev/null || echo " 0" )
125+
126+ if [ " $index_key_count " -gt 0 ]; then
127+ break # Index has entries — proceed normally
128+ fi
129+
130+ # Index is empty — check if tracker dir has entries (indicating transient failure)
131+ if ! _tracker_has_entries; then
132+ break # Tracker genuinely has no tickets — no point retrying
133+ fi
134+
135+ # Tracker has entries but reducer returned empty — transient failure, retry
136+ attempt=$(( attempt + 1 ))
137+ sleep " $RETRY_WAIT "
138+ index_and_counts=$( _build_index)
139+ done
94140
95141SPRINT_INDEX_JSON=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['index']))" )
96142child_counts_json=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['child_counts']))" )
0 commit comments