Skip to content

Commit 17ba188

Browse files
LCORE-1632 Feature design process: improvements (#1445)
* Add fetch-jira.sh, shared jira credentials, update /spike skill to accept JIRA numbers, fix templates 404 * Add JIRA category guidance (impl/integration/e2e/docs), update file-jiras skill credentials * Rewrite file-jiras.sh: Epic+children hierarchy, type metadata, named args, --help * Update skills, howto, and contributing guide for new file-jiras syntax and spec-doc spike search * Add <!-- key: LCORE-XXXX --> support: update existing tickets, skip re-parse when files exist * Fix file-jiras.sh: barebones Epic, skip dup check for updates, re-parent on update, fix naming and UX * file-jiras.sh: default dir in repo, barebones Epic, spike doc key substitution after filing * Fix file-jiras.sh: Epic key refresh prevents hang, remove script-based spike doc substitution, better spike key detection * Fix file-jiras.sh: refresh EPIC_KEY after filing to fix subshell propagation hang * Fix local-in-case error, change Filed to Done in output * AI-workflow-first contributing guide, LCORE-xxxx key placeholders, spike doc substitution step * Improvements in contributing guide and howto-run-a-spike.md * Fix shellcheck SC2059: use printf %s instead of variable in format string * Address CodeRabbit feedback: option validation, portable sed, Epic key safety, stale references * Fix shellcheck: suppress SC2034 in jira-common.sh, SC1091 in sourcing scripts
1 parent ab729a0 commit 17ba188

11 files changed

Lines changed: 740 additions & 170 deletions

File tree

.claude/commands/file-jiras.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,32 @@ Parse proposed JIRAs from a spike doc and file them via the Jira API
33
You are filing JIRA sub-tickets for a Lightspeed Core feature.
44

55
The user will provide either a spike doc path or tell you which feature's
6-
JIRAs to file. They will also provide the parent JIRA ticket number.
6+
JIRAs to file. They will also provide the feature ticket number.
7+
8+
Run `sh dev-tools/file-jiras.sh --help` to see the full usage.
79

810
## Credentials
911

10-
Jira credentials must be in `~/.config/jira/credentials.json`. If this file
11-
doesn't exist, tell the user to create it (see `dev-tools/file-jiras.sh` for
12-
the format and instructions).
12+
Jira credentials are managed by `dev-tools/jira-common.sh`. If
13+
`~/.config/jira/credentials.json` doesn't exist, the script creates it
14+
with FIXMEs and exits — the user must fill in their credentials before
15+
re-running. API tokens can be created at
16+
https://id.atlassian.com/manage-profile/security/api-tokens
1317

1418
## Process
1519

16-
1. Run `dev-tools/file-jiras.sh <spike-doc.md> <parent-ticket>` with
17-
`echo "quit"` piped in, so it parses and exits without filing.
20+
1. Run `dev-tools/file-jiras.sh --spike-doc <path> --feature-ticket <key>`
21+
with `echo "quit"` piped in, so it parses and exits without filing.
1822

19-
2. Read every file in `/tmp/jiras/`. For each, verify:
23+
2. Read every file in the output directory (default: `docs/design/<feature>/jiras/`).
24+
For each, verify:
2025
- Content matches the corresponding section in the spike doc (no truncation,
2126
no extra content swallowed from subsequent sections).
27+
- File size is reasonable (a single JIRA should be under ~3KB; if any file
28+
is much larger, the parser likely grabbed too much).
29+
- The `<!-- type: ... -->` metadata is correct (Epic/Story/Task).
2230

2331
3. Report any issues to the user. If all files look correct, tell the user
2432
to run the script interactively — provide the full command including `cd`
2533
to the repository root:
26-
`cd <repo-path> && sh dev-tools/file-jiras.sh <spike-doc.md> <parent-ticket>`
34+
`cd <repo-path> && sh dev-tools/file-jiras.sh --spike-doc <path> --feature-ticket <key>`

.claude/commands/spec-doc.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@ You are creating a feature spec doc for the Lightspeed Core project.
55
Follow the guidance in `docs/contributing/howto-write-a-spec-doc.md`. Use
66
`docs/contributing/templates/spec-doc-template.md` as the starting point.
77

8-
The user will provide context about the feature — a spike doc, a description,
9-
or a conversation. Read what is provided and ask for anything missing.
8+
If the user provides a JIRA ticket number, look for an existing spike doc:
9+
1. Search filenames in `docs/design/*/` for the JIRA number (bare and with
10+
LCORE- prefix) and for words likely related to the feature.
11+
2. If not found by filename, grep inside files in `docs/design/*/` for the
12+
JIRA number.
13+
3. If a spike doc is found, use it as the primary source for the spec doc.
14+
4. If no spike doc exists, let the user know and ask them to provide the path
15+
if one exists elsewhere. Otherwise, fetch the JIRA content with
16+
`sh dev-tools/fetch-jira.sh <number>` and work from that.
17+
18+
The user may also provide a spike doc path or feature description directly.
1019

1120
Place the spec doc at `docs/design/<feature>/<feature>.md`. Confirm the
1221
feature name and path with the user.

.claude/commands/spike.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ You are starting a spike for a feature in the Lightspeed Core project.
55
Follow the process in `docs/contributing/howto-run-a-spike.md`. Use the
66
templates it references.
77

8-
The user will provide context about the feature — a JIRA ticket, a description,
9-
or a conversation. Read what is provided and ask for anything missing.
8+
If the user provides a JIRA ticket number (e.g., "1234" or "LCORE-1234"),
9+
fetch the ticket content by running `sh dev-tools/fetch-jira.sh <number>`.
10+
The output includes child issues — decide which linked tickets to fetch
11+
for additional context.
12+
13+
Otherwise, the user will provide context about the feature directly.
14+
15+
When proposing JIRAs in the spike doc, specify the type for each ticket
16+
using `<!-- type: Task -->` or `<!-- type: Story -->` (see the JIRA ticket
17+
template). Use `dev-tools/file-jiras.sh --help` for filing details.
1018

1119
At decision points, present what you've found and ask the user before proceeding.
1220
The user makes the decisions — you assist with the research and documenting.

dev-tools/fetch-jira.sh

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env bash
2+
# Fetch JIRA ticket content and its linked/child tickets.
3+
#
4+
# Usage:
5+
# fetch-jira.sh <ticket>
6+
# fetch-jira.sh 1234 (defaults to LCORE-1234)
7+
# fetch-jira.sh LCORE-1234
8+
#
9+
# Prerequisites:
10+
# ~/.config/jira/credentials.json with email, token, instance.
11+
#
12+
# Output: ticket summary, description, acceptance criteria, status,
13+
# and linked/child tickets (fetched recursively one level deep).
14+
15+
set -euo pipefail
16+
17+
# shellcheck disable=SC1091
18+
. "$(dirname "$0")/jira-common.sh"
19+
20+
if [ $# -lt 1 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
21+
echo "Usage: fetch-jira.sh <ticket> [additional-tickets...]"
22+
echo ""
23+
echo "Fetches JIRA ticket content including description, status, and child issues."
24+
echo "Bare numbers default to LCORE- prefix."
25+
echo ""
26+
echo "Examples:"
27+
echo " fetch-jira.sh 1234 Fetch LCORE-1234"
28+
echo " fetch-jira.sh LCORE-1234 Same"
29+
echo " fetch-jira.sh 836 509 777 Fetch multiple tickets"
30+
if [ $# -lt 1 ]; then exit 1; else exit 0; fi
31+
fi
32+
33+
ensure_jira_credentials
34+
35+
TICKET="$1"
36+
# If bare number, prepend LCORE-
37+
if echo "$TICKET" | grep -qE '^[0-9]+$'; then
38+
TICKET="LCORE-$TICKET"
39+
fi
40+
41+
fetch_ticket() {
42+
local key="$1"
43+
local indent="${2:-}"
44+
45+
local data
46+
data=$(curl -sS --connect-timeout 10 --max-time 30 \
47+
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
48+
"$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null)
49+
50+
if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then
51+
python3 -c "
52+
import json, sys, textwrap
53+
54+
data = json.loads(sys.argv[1])
55+
indent = sys.argv[2]
56+
key = data['key']
57+
fields = data['fields']
58+
summary = fields['summary']
59+
status = fields['status']['name']
60+
issue_type = fields['issuetype']['name']
61+
parent = fields.get('parent', {})
62+
parent_key = parent.get('key', '') if parent else ''
63+
64+
print(f'{indent}=== {key}: {summary} ===')
65+
print(f'{indent}Type: {issue_type} | Status: {status}')
66+
if parent_key:
67+
print(f'{indent}Parent: {parent_key}')
68+
print()
69+
70+
# Description
71+
desc = fields.get('description')
72+
if desc and isinstance(desc, dict):
73+
# ADF format — extract text
74+
def extract_text(node, depth=0):
75+
lines = []
76+
if isinstance(node, dict):
77+
ntype = node.get('type', '')
78+
if ntype == 'text':
79+
text = node.get('text', '')
80+
marks = node.get('marks', [])
81+
for m in marks:
82+
if m.get('type') == 'strong':
83+
text = f'**{text}**'
84+
elif m.get('type') == 'code':
85+
text = f'\`{text}\`'
86+
return [text]
87+
if ntype == 'hardBreak':
88+
return ['\n']
89+
if ntype == 'listItem':
90+
child_text = []
91+
for c in node.get('content', []):
92+
child_text.extend(extract_text(c, depth))
93+
return [' ' * depth + '- ' + ''.join(child_text).strip()]
94+
if ntype in ('bulletList', 'orderedList'):
95+
for c in node.get('content', []):
96+
lines.extend(extract_text(c, depth + 1))
97+
return lines
98+
if ntype == 'heading':
99+
level = node.get('attrs', {}).get('level', 1)
100+
child_text = []
101+
for c in node.get('content', []):
102+
child_text.extend(extract_text(c, depth))
103+
return ['#' * level + ' ' + ''.join(child_text).strip()]
104+
if ntype == 'codeBlock':
105+
child_text = []
106+
for c in node.get('content', []):
107+
child_text.extend(extract_text(c, depth))
108+
return ['\`\`\`\n' + ''.join(child_text) + '\n\`\`\`']
109+
for c in node.get('content', []):
110+
lines.extend(extract_text(c, depth))
111+
if ntype == 'paragraph' and lines:
112+
lines.append('')
113+
return lines
114+
115+
text_lines = extract_text(desc)
116+
desc_text = '\n'.join(text_lines).strip()
117+
if desc_text:
118+
for line in desc_text.split('\n'):
119+
print(f'{indent}{line}')
120+
print()
121+
122+
# Links
123+
links = fields.get('issuelinks', [])
124+
if links:
125+
print(f'{indent}Linked issues:')
126+
for link in links:
127+
link_type = link.get('type', {}).get('name', '?')
128+
if 'outwardIssue' in link:
129+
linked = link['outwardIssue']
130+
direction = link.get('type', {}).get('outward', 'relates to')
131+
elif 'inwardIssue' in link:
132+
linked = link['inwardIssue']
133+
direction = link.get('type', {}).get('inward', 'relates to')
134+
else:
135+
continue
136+
lkey = linked['key']
137+
lsummary = linked['fields']['summary']
138+
lstatus = linked['fields']['status']['name']
139+
print(f'{indent} {direction}: {lkey} — {lsummary} [{lstatus}]')
140+
print()
141+
142+
# Subtasks
143+
subtasks = fields.get('subtasks', [])
144+
if subtasks:
145+
print(f'{indent}Child issues:')
146+
for st in subtasks:
147+
skey = st['key']
148+
ssummary = st['fields']['summary']
149+
sstatus = st['fields']['status']['name']
150+
print(f'{indent} {skey} — {ssummary} [{sstatus}]')
151+
print()
152+
" "$data" "$indent"
153+
else
154+
echo "${indent}Error fetching $key"
155+
echo "$data" | head -3
156+
fi
157+
}
158+
159+
# Fetch main ticket
160+
fetch_ticket "$TICKET"
161+
162+
# Search for child issues via parent= JQL (Jira Cloud hierarchy)
163+
CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \
164+
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
165+
"$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \
166+
python3 -c "
167+
import json, sys
168+
try:
169+
data = json.load(sys.stdin)
170+
for issue in data.get('issues', []):
171+
key = issue['key']
172+
summary = issue['fields']['summary']
173+
status = issue['fields']['status']['name']
174+
itype = issue['fields']['issuetype']['name']
175+
print(f'{key} ({itype}) [{status}]: {summary}')
176+
except Exception:
177+
pass
178+
" 2>/dev/null)
179+
180+
if [ -n "$CHILD_KEYS" ]; then
181+
echo "Child issues:"
182+
echo "$CHILD_KEYS" | while read -r line; do
183+
echo " $line"
184+
done
185+
echo ""
186+
fi
187+
188+
# If additional ticket keys are passed as arguments, fetch those too
189+
shift
190+
for extra in "$@"; do
191+
if echo "$extra" | grep -qE '^[0-9]+$'; then
192+
extra="LCORE-$extra"
193+
fi
194+
echo "────────────────────────────────────────────────────────"
195+
echo ""
196+
fetch_ticket "$extra"
197+
done

0 commit comments

Comments
 (0)