22set -euo pipefail
33# sprint-list-epics.sh — List unblocked epics for /dso:sprint Phase 1.
44#
5- # Reads .tickets/.index.json in a single Python pass instead of per-file scanning.
6- # Blocked/ready classification uses the deps field from the extended index schema.
5+ # Reads ticket state from the v3 event-sourced tracker via ticket-reducer.py.
76#
87# Usage:
98# sprint-list-epics.sh # List unblocked open epics, sorted by priority
@@ -27,46 +26,20 @@ show_all=false
2726[[ " ${1:- } " == " --all" ]] && show_all=true
2827
2928REPO_ROOT=$( git rev-parse --show-toplevel)
30- # Capture whether TICKETS_DIR was explicitly set by caller before applying defaults
31- _TICKETS_DIR_EXPLICIT=" ${TICKETS_DIR+yes} "
32- TICKETS_DIR=" ${TICKETS_DIR:- $REPO_ROOT / .tickets} "
33- INDEX_FILE=" $TICKETS_DIR /.index.json"
34- TK=" ${TK:- $SCRIPT_DIR / tk} "
3529REDUCER=" $SCRIPT_DIR /ticket-reducer.py"
3630
3731# ---------------------------------------------------------------------------
38- # Detect v3 event-sourced ticket system.
39- # v3 stores events in .tickets-tracker/ (or TICKETS_TRACKER_DIR env override).
40- # When v3 is detected, build the index from the reducer instead of .md files.
32+ # v3 event-sourced ticket system.
33+ # Reads from .tickets-tracker/ (or TICKETS_TRACKER_DIR env override).
4134# ---------------------------------------------------------------------------
42- # Detection logic:
43- # - TICKETS_TRACKER_DIR explicitly set → v3 (test override for v3)
44- # - TICKETS_DIR explicitly set without TICKETS_TRACKER_DIR → v2 (test override for v2)
45- # - Neither explicitly set → auto-detect: use v3 if .tickets-tracker/ exists
46- USE_V3=false
47- if [ -n " ${TICKETS_TRACKER_DIR:- } " ]; then
48- TRACKER_DIR=" $TICKETS_TRACKER_DIR "
49- USE_V3=true
50- elif [ " $_TICKETS_DIR_EXPLICIT " != " yes" ]; then
51- # TICKETS_DIR not explicitly set — auto-detect
52- TRACKER_DIR=" $REPO_ROOT /.tickets-tracker"
53- if [ -d " $TRACKER_DIR " ]; then
54- USE_V3=true
55- fi
56- else
57- # TICKETS_DIR explicitly set, TICKETS_TRACKER_DIR not — use v2
58- TRACKER_DIR=" $REPO_ROOT /.tickets-tracker"
59- fi
35+ TRACKER_DIR=" ${TICKETS_TRACKER_DIR:- $REPO_ROOT / .tickets-tracker} "
6036
6137# ---------------------------------------------------------------------------
62- # Build index from data source (v3 reducer or v2 .md files).
63- # Both paths produce the same index format in SPRINT_INDEX_JSON env var.
38+ # Build index from v3 reducer.
6439# ---------------------------------------------------------------------------
65- if [ " $USE_V3 " = true ]; then
66- # v3 path: compile ticket state from event-sourced tracker via reducer
67- export _SPRINT_TRACKER_DIR=" $TRACKER_DIR "
68- export _SPRINT_REDUCER=" $REDUCER "
69- index_and_counts=$( python3 -c "
40+ export _SPRINT_TRACKER_DIR=" $TRACKER_DIR "
41+ export _SPRINT_REDUCER=" $REDUCER "
42+ index_and_counts=$( python3 -c "
7043import json, os, sys, importlib.util, collections
7144
7245tracker_dir = os.environ['_SPRINT_TRACKER_DIR']
@@ -119,169 +92,8 @@ for entry_name in os.listdir(tracker_dir):
11992print(json.dumps({'index': idx, 'child_counts': dict(child_counts)}))
12093" 2> /dev/null || echo ' {"index":{},"child_counts":{}}' )
12194
122- SPRINT_INDEX_JSON=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['index']))" )
123- child_counts_json=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['child_counts']))" )
124- else
125- # v2 path: read .tickets/*.md files and build index
126-
127- # Staleness guard: compare .md file count vs index entry count.
128- _rebuild_index () {
129- if [ -x " $TK " ] || command -v " $TK " > /dev/null 2>&1 ; then
130- TICKETS_DIR=" $TICKETS_DIR " " $TK " index-rebuild > /dev/null 2>&1 || true
131- else
132- python3 -c "
133- import json, os, re, sys
134-
135- tickets_dir = os.environ.get('TICKETS_DIR', '.tickets')
136- idx = {}
137-
138- try:
139- files = [f for f in os.listdir(tickets_dir) if f.endswith('.md')]
140- except OSError:
141- files = []
142-
143- for fname in files:
144- fpath = os.path.join(tickets_dir, fname)
145- try:
146- content = open(fpath).read()
147- except OSError:
148- continue
149-
150- lines = content.splitlines()
151- in_front = False
152- front_lines = []
153- count = 0
154- for line in lines:
155- if line.strip() == '---':
156- count += 1
157- if count == 1:
158- in_front = True
159- continue
160- elif count == 2:
161- in_front = False
162- break
163- if in_front:
164- front_lines.append(line)
165-
166- def get_field(name):
167- for l in front_lines:
168- m = re.match(r'^' + re.escape(name) + r':\s*(.*)', l)
169- if m:
170- return m.group(1).strip()
171- return ''
172-
173- ticket_id = get_field('id')
174- if not ticket_id:
175- ticket_id = fname[:-3]
176-
177- status = get_field('status') or 'open'
178- type_ = get_field('type') or 'task'
179-
180- raw_priority = get_field('priority')
181- try:
182- priority = int(raw_priority) if raw_priority != '' else None
183- except (ValueError, TypeError):
184- priority = None
185-
186- raw_deps = get_field('deps')
187- if raw_deps in ('', '[]'):
188- deps = []
189- else:
190- inner = raw_deps.strip().lstrip('[').rstrip(']')
191- deps = [s.strip().strip('\" ').strip(chr(39)) for s in inner.split(',') if s.strip()]
192-
193- title = ''
194- for line in lines:
195- if line.startswith('# '):
196- title = line[2:].strip()
197- break
198-
199- parent = get_field('parent')
200-
201- entry = {'title': title, 'status': status, 'type': type_}
202- if priority is not None:
203- entry['priority'] = priority
204- if deps:
205- entry['deps'] = deps
206- if parent:
207- entry['parent'] = parent
208- idx[ticket_id] = entry
209-
210- # Write atomically
211- import tempfile
212- tmp = os.path.join(tickets_dir, '.index.json.tmp')
213- with open(tmp, 'w') as f:
214- json.dump(idx, f, indent=2, sort_keys=True)
215- os.replace(tmp, os.path.join(tickets_dir, '.index.json'))
216- " 2> /dev/null || true
217- fi
218- }
219-
220- _check_staleness () {
221- local index_count md_count
222- md_count=$( python3 -c "
223- import os
224- d = '$TICKETS_DIR '
225- try:
226- print(sum(1 for f in os.listdir(d) if f.endswith('.md')))
227- except OSError:
228- print(0)
229- " 2> /dev/null || echo 0)
230- index_count=$( python3 -c "
231- import json
232- try:
233- with open('$INDEX_FILE ') as f:
234- data = json.load(f)
235- print(len(data))
236- except Exception:
237- print(-1)
238- " 2> /dev/null || echo -1)
239- if [ " $index_count " -ne " $md_count " ]; then
240- _rebuild_index
241- fi
242- }
243-
244- _check_staleness
245-
246- # Read index file for v2 path
247- SPRINT_INDEX_JSON=$( cat " $INDEX_FILE " 2> /dev/null || echo ' {}' )
248-
249- # Compute child counts from .md files
250- child_counts_json=$( python3 -c "
251- import os, re, collections, json
252-
253- tickets_dir = '$TICKETS_DIR '
254- counts = collections.defaultdict(int)
255-
256- try:
257- files = [f for f in os.listdir(tickets_dir) if f.endswith('.md')]
258- except OSError:
259- files = []
260-
261- for fname in files:
262- fpath = os.path.join(tickets_dir, fname)
263- try:
264- content = open(fpath).read()
265- except OSError:
266- continue
267- in_front = False
268- front_count = 0
269- for line in content.splitlines():
270- if line.strip() == '---':
271- front_count += 1
272- in_front = (front_count == 1)
273- if front_count == 2:
274- break
275- continue
276- if in_front:
277- m = re.match(r'^parent:\s*(\S+)', line)
278- if m:
279- parent_id = m.group(1).rstrip('#').strip()
280- counts[parent_id] += 1
281-
282- print(json.dumps(dict(counts)))
283- " 2> /dev/null || echo ' {}' )
284- fi
95+ SPRINT_INDEX_JSON=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['index']))" )
96+ child_counts_json=$( echo " $index_and_counts " | python3 -c " import json,sys; print(json.dumps(json.load(sys.stdin)['child_counts']))" )
28597
28698# ---------------------------------------------------------------------------
28799# Single Python pass: read index once, classify epics, emit output.
@@ -291,7 +103,7 @@ import json, os, sys
291103
292104show_all = os.environ.get('SPRINT_SHOW_ALL') == 'true'
293105
294- # Load index from env var (built by v2 or v3 path above)
106+ # Load index from env var (built by v3 path above)
295107try:
296108 index = json.loads(os.environ.get('SPRINT_INDEX_JSON', '{}'))
297109except Exception:
0 commit comments