Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
7467507
Add fetch-jira.sh, shared jira credentials, update /spike skill to ac…
max-svistunov Mar 31, 2026
5aa09f1
Add JIRA category guidance (impl/integration/e2e/docs), update file-j…
max-svistunov Mar 31, 2026
c03eecd
Rewrite file-jiras.sh: Epic+children hierarchy, type metadata, named …
max-svistunov Mar 31, 2026
0720d24
Update skills, howto, and contributing guide for new file-jiras synta…
max-svistunov Apr 1, 2026
f60cb7d
Add <!-- key: LCORE-XXXX --> support: update existing tickets, skip r…
max-svistunov Apr 1, 2026
227125f
Fix file-jiras.sh: barebones Epic, skip dup check for updates, re-par…
max-svistunov Apr 1, 2026
3df0385
file-jiras.sh: default dir in repo, barebones Epic, spike doc key sub…
max-svistunov Apr 1, 2026
d712787
Fix file-jiras.sh: Epic key refresh prevents hang, remove script-base…
max-svistunov Apr 1, 2026
7664ddc
Fix file-jiras.sh: refresh EPIC_KEY after filing to fix subshell prop…
max-svistunov Apr 1, 2026
ec902d8
Fix local-in-case error, change Filed to Done in output
max-svistunov Apr 1, 2026
269853d
AI-workflow-first contributing guide, LCORE-xxxx key placeholders, sp…
max-svistunov Apr 1, 2026
8d58dfa
Improvements in contributing guide and howto-run-a-spike.md
max-svistunov Apr 1, 2026
fd3cf57
Fix shellcheck SC2059: use printf %s instead of variable in format st…
max-svistunov Apr 1, 2026
8b6088c
Address CodeRabbit feedback: option validation, portable sed, Epic ke…
max-svistunov Apr 1, 2026
c79bf25
Fix shellcheck: suppress SC2034 in jira-common.sh, SC1091 in sourcing…
max-svistunov Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions .claude/commands/file-jiras.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@ Parse proposed JIRAs from a spike doc and file them via the Jira API
You are filing JIRA sub-tickets for a Lightspeed Core feature.

The user will provide either a spike doc path or tell you which feature's
JIRAs to file. They will also provide the parent JIRA ticket number.
JIRAs to file. They will also provide the feature ticket number.

Run `sh dev-tools/file-jiras.sh --help` to see the full usage.

## Credentials

Jira credentials must be in `~/.config/jira/credentials.json`. If this file
doesn't exist, tell the user to create it (see `dev-tools/file-jiras.sh` for
the format and instructions).
Jira credentials are managed by `dev-tools/jira-common.sh`. If
`~/.config/jira/credentials.json` doesn't exist, the script creates it
with FIXMEs and exits — the user must fill in their credentials before
re-running. API tokens can be created at
https://id.atlassian.com/manage-profile/security/api-tokens
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Process

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

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

3. Report any issues to the user. If all files look correct, tell the user
to run the script interactively — provide the full command including `cd`
to the repository root:
`cd <repo-path> && sh dev-tools/file-jiras.sh <spike-doc.md> <parent-ticket>`
`cd <repo-path> && sh dev-tools/file-jiras.sh --spike-doc <path> --feature-ticket <key>`
13 changes: 11 additions & 2 deletions .claude/commands/spec-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ You are creating a feature spec doc for the Lightspeed Core project.
Follow the guidance in `docs/contributing/howto-write-a-spec-doc.md`. Use
`docs/contributing/templates/spec-doc-template.md` as the starting point.

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

The user may also provide a spike doc path or feature description directly.

Place the spec doc at `docs/design/<feature>/<feature>.md`. Confirm the
feature name and path with the user.
12 changes: 10 additions & 2 deletions .claude/commands/spike.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ You are starting a spike for a feature in the Lightspeed Core project.
Follow the process in `docs/contributing/howto-run-a-spike.md`. Use the
templates it references.

The user will provide context about the feature — a JIRA ticket, a description,
or a conversation. Read what is provided and ask for anything missing.
If the user provides a JIRA ticket number (e.g., "1234" or "LCORE-1234"),
fetch the ticket content by running `sh dev-tools/fetch-jira.sh <number>`.
The output includes child issues — decide which linked tickets to fetch
for additional context.
Comment on lines +10 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Make parsing instructions deterministic by naming exact output section headers.

To reduce ambiguity, reference the exact fetch output labels (Child issues: and Linked issues:) so the agent consistently extracts the right blocks before deciding follow-up fetches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/commands/spike.md around lines 10 - 11, Update the parsing
instruction text in .claude/commands/spike.md to explicitly name and require the
exact output section headers "Child issues:" and "Linked issues:" so the agent
deterministically extracts those blocks; change any vague references to
"child/linked issues" to the exact labels and add a short directive to only read
content under those headers (ignore similarly named lines elsewhere) before
deciding which linked tickets to fetch for follow-up.


Otherwise, the user will provide context about the feature directly.

When proposing JIRAs in the spike doc, specify the type for each ticket
using `<!-- type: Task -->` or `<!-- type: Story -->` (see the JIRA ticket
template). Use `dev-tools/file-jiras.sh --help` for filing details.

At decision points, present what you've found and ask the user before proceeding.
The user makes the decisions — you assist with the research and documenting.
197 changes: 197 additions & 0 deletions dev-tools/fetch-jira.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env bash
# Fetch JIRA ticket content and its linked/child tickets.
#
# Usage:
# fetch-jira.sh <ticket>
# fetch-jira.sh 1234 (defaults to LCORE-1234)
# fetch-jira.sh LCORE-1234
#
# Prerequisites:
# ~/.config/jira/credentials.json with email, token, instance.
#
# Output: ticket summary, description, acceptance criteria, status,
# and linked/child tickets (fetched recursively one level deep).

set -euo pipefail

# shellcheck disable=SC1091
. "$(dirname "$0")/jira-common.sh"

if [ $# -lt 1 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Usage: fetch-jira.sh <ticket> [additional-tickets...]"
echo ""
echo "Fetches JIRA ticket content including description, status, and child issues."
echo "Bare numbers default to LCORE- prefix."
echo ""
echo "Examples:"
echo " fetch-jira.sh 1234 Fetch LCORE-1234"
echo " fetch-jira.sh LCORE-1234 Same"
echo " fetch-jira.sh 836 509 777 Fetch multiple tickets"
if [ $# -lt 1 ]; then exit 1; else exit 0; fi
fi

ensure_jira_credentials

TICKET="$1"
# If bare number, prepend LCORE-
if echo "$TICKET" | grep -qE '^[0-9]+$'; then
TICKET="LCORE-$TICKET"
fi

fetch_ticket() {
local key="$1"
local indent="${2:-}"

local data
data=$(curl -sS --connect-timeout 10 --max-time 30 \
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
"$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null)

Comment on lines +45 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Silent failure on curl errors may hide connectivity issues.

Redirecting stderr to /dev/null on the curl command suppresses connection errors, timeouts, and SSL issues. While the JSON parsing check on line 50 catches missing data, users won't see why a fetch failed.

Proposed fix to preserve error visibility
     local data
-    data=$(curl -sS --connect-timeout 10 --max-time 30 \
+    data=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 30 \
         -u "$JIRA_EMAIL:$JIRA_TOKEN" \
-        "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null)
+        "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>&1) || true

This preserves error messages while still allowing the script to continue and report the error on lines 153-156.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local data
data=$(curl -sS --connect-timeout 10 --max-time 30 \
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
"$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null)
local data
data=$(curl -sS --fail-with-body --connect-timeout 10 --max-time 30 \
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
"$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>&1) || true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev-tools/fetch-jira.sh` around lines 45 - 49, The curl call that populates
the local variable data currently redirects stderr to /dev/null, hiding
connection/SSL/timeouts; remove the trailing "2>/dev/null" (or capture stderr to
a separate variable if you prefer) in the curl invocation that uses $JIRA_EMAIL,
$JIRA_TOKEN and $JIRA_INSTANCE so connection errors are visible when the script
fails JSON parsing of data; keep the existing JSON presence/error handling logic
so the script still reports failures downstream.

if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then
python3 -c "
import json, sys, textwrap

data = json.loads(sys.argv[1])
indent = sys.argv[2]
key = data['key']
fields = data['fields']
summary = fields['summary']
status = fields['status']['name']
issue_type = fields['issuetype']['name']
parent = fields.get('parent', {})
parent_key = parent.get('key', '') if parent else ''

print(f'{indent}=== {key}: {summary} ===')
print(f'{indent}Type: {issue_type} | Status: {status}')
if parent_key:
print(f'{indent}Parent: {parent_key}')
print()

# Description
desc = fields.get('description')
if desc and isinstance(desc, dict):
# ADF format — extract text
def extract_text(node, depth=0):
lines = []
if isinstance(node, dict):
ntype = node.get('type', '')
if ntype == 'text':
text = node.get('text', '')
marks = node.get('marks', [])
for m in marks:
if m.get('type') == 'strong':
text = f'**{text}**'
elif m.get('type') == 'code':
text = f'\`{text}\`'
return [text]
if ntype == 'hardBreak':
return ['\n']
if ntype == 'listItem':
child_text = []
for c in node.get('content', []):
child_text.extend(extract_text(c, depth))
return [' ' * depth + '- ' + ''.join(child_text).strip()]
if ntype in ('bulletList', 'orderedList'):
for c in node.get('content', []):
lines.extend(extract_text(c, depth + 1))
return lines
if ntype == 'heading':
level = node.get('attrs', {}).get('level', 1)
child_text = []
for c in node.get('content', []):
child_text.extend(extract_text(c, depth))
return ['#' * level + ' ' + ''.join(child_text).strip()]
if ntype == 'codeBlock':
child_text = []
for c in node.get('content', []):
child_text.extend(extract_text(c, depth))
return ['\`\`\`\n' + ''.join(child_text) + '\n\`\`\`']
for c in node.get('content', []):
lines.extend(extract_text(c, depth))
if ntype == 'paragraph' and lines:
lines.append('')
return lines

text_lines = extract_text(desc)
desc_text = '\n'.join(text_lines).strip()
if desc_text:
for line in desc_text.split('\n'):
print(f'{indent}{line}')
print()

# Links
links = fields.get('issuelinks', [])
if links:
print(f'{indent}Linked issues:')
for link in links:
link_type = link.get('type', {}).get('name', '?')
if 'outwardIssue' in link:
linked = link['outwardIssue']
direction = link.get('type', {}).get('outward', 'relates to')
elif 'inwardIssue' in link:
linked = link['inwardIssue']
direction = link.get('type', {}).get('inward', 'relates to')
else:
continue
lkey = linked['key']
lsummary = linked['fields']['summary']
lstatus = linked['fields']['status']['name']
print(f'{indent} {direction}: {lkey} — {lsummary} [{lstatus}]')
print()

# Subtasks
subtasks = fields.get('subtasks', [])
if subtasks:
print(f'{indent}Child issues:')
for st in subtasks:
skey = st['key']
ssummary = st['fields']['summary']
sstatus = st['fields']['status']['name']
print(f'{indent} {skey} — {ssummary} [{sstatus}]')
print()
" "$data" "$indent"
else
echo "${indent}Error fetching $key"
echo "$data" | head -3
fi
}
Comment on lines +41 to +157

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

ADF parsing is comprehensive but extract_text recursion could hit Python's default limit.

Deeply nested Jira descriptions (e.g., 1000+ nested list levels) would cause RecursionError. This is unlikely with real Jira content but worth noting.

The error handling on line 153-156 provides a reasonable fallback when fetch fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev-tools/fetch-jira.sh` around lines 41 - 157, The recursive extract_text
function inside fetch_ticket can hit Python's recursion limit for deeply nested
ADF nodes; replace it with an iterative stack-based traversal (or add a
try/except around the current call to catch RecursionError and fall back to a
safe non-recursive extraction) so extremely deep descriptions won't crash the
script. Locate extract_text in the Python block invoked by fetch_ticket,
implement a stack/loop that processes nodes and their depth (preserving handling
for types like text, listItem, bulletList/orderedList, heading, codeBlock,
paragraph, hardBreak) or wrap json.loads/sys.argv processing in try/except
RecursionError and produce a simple plaintext fallback (e.g., strip markup and
join visible text) when recursion fails. Ensure the fallback still prints the
key/summary/status and doesn't abort the shell function.


# Fetch main ticket
fetch_ticket "$TICKET"

# Search for child issues via parent= JQL (Jira Cloud hierarchy)
CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \
-u "$JIRA_EMAIL:$JIRA_TOKEN" \
"$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \
python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
for issue in data.get('issues', []):
key = issue['key']
summary = issue['fields']['summary']
status = issue['fields']['status']['name']
itype = issue['fields']['issuetype']['name']
print(f'{key} ({itype}) [{status}]: {summary}')
except Exception:
pass
" 2>/dev/null)
Comment on lines +162 to +178

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Child issues query silently swallows all errors.

The 2>/dev/null at the end of the curl pipe and except Exception: pass in the Python block mean any API errors (auth failures, rate limits, invalid JQL) are silently ignored. Consider logging or at least not suppressing the curl stderr.

Proposed improvement
 CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \
     -u "$JIRA_EMAIL:$JIRA_TOKEN" \
-    "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \
+    "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" | \
     python3 -c "
 import json, sys
 try:
     data = json.load(sys.stdin)
     for issue in data.get('issues', []):
         key = issue['key']
         summary = issue['fields']['summary']
         status = issue['fields']['status']['name']
         itype = issue['fields']['issuetype']['name']
         print(f'{key} ({itype}) [{status}]: {summary}')
-except Exception:
-    pass
+except Exception as e:
+    print(f'Warning: Could not fetch child issues: {e}', file=sys.stderr)
 " 2>/dev/null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev-tools/fetch-jira.sh` around lines 162 - 178, The CHILD_KEYS pipeline
currently hides errors because the curl call redirects stderr to /dev/null and
the Python block swallows exceptions; update the pipeline around CHILD_KEYS so
curl does not discard stderr (remove the trailing 2>/dev/null) and change the
python3 block in the heredoc to surface failures (replace the bare except/ pass
with printing a meaningful error to stderr and non-zero exit, or re-raise the
exception) and/or check curl's exit status before piping into python3 so
authentication/JQL/API errors are visible; refer to the CHILD_KEYS assignment,
the curl invocation, and the python3 block to locate and change these behaviors.


if [ -n "$CHILD_KEYS" ]; then
echo "Child issues:"
echo "$CHILD_KEYS" | while read -r line; do
echo " $line"
done
echo ""
fi

# If additional ticket keys are passed as arguments, fetch those too
shift
for extra in "$@"; do
if echo "$extra" | grep -qE '^[0-9]+$'; then
extra="LCORE-$extra"
fi
echo "────────────────────────────────────────────────────────"
echo ""
fetch_ticket "$extra"
done
Loading
Loading