| name | jira-integration |
| description | Master Jira integration using acli CLI, Jira REST API, issue management, sprint operations, JQL queries, and ADF comment formatting. Essential for Jira-based project management automation. |
| triggers | jira integration, acli, atlassian cli, jira api, jql query, jira sprint, adf format, jira comment, jira issue detection |
Jira Integration Mastery
This skill provides comprehensive guidance for integrating with Jira using the Atlassian CLI (acli), Jira REST API, and ADF (Atlassian Document Format). Essential for automating issue management, sprint planning, JQL queries, and building robust Jira-based workflows.
When to Use This Skill
- Automating Jira issue creation, updates, and transitions
- Building sprint planning workflows
- Writing and executing JQL queries
- Formatting comments with ADF (Atlassian Document Format)
- Parsing Jira URLs and extracting metadata
- Integrating Jira into development automation
- Creating bulk operations across multiple issues
Atlassian CLI (acli) Mastery
The Atlassian CLI (acli) is the primary tool for Jira automation via command line.
Installation and Configuration
# Download acli
curl -O https://bobswift.atlassian.net/wiki/download/attachments/16285777/acli-9.8.0-distribution.zip
# Extract and setup
unzip acli-9.8.0-distribution.zip
export PATH=$PATH:/path/to/acli
# Configure connection
acli jira --server https://your-domain.atlassian.net --user user@example.com --password your-api-token --action getServerInfo
# Store credentials (creates ~/.acli/acli.properties)
acli jira --server https://your-domain.atlassian.net --user user@example.com --password your-api-token --action login
Configuration file (~/.acli/acli.properties):
server=https://your-domain.atlassian.net
user=user@example.com
password=your-api-token
Issue Operations
List issues:
# List issues in project
acli jira --action getIssueList --project PROJ
# List with JQL
acli jira --action getIssueList --jql "project = PROJ AND status = 'To Do'"
# List with specific fields
acli jira --action getIssueList --jql "assignee = currentUser()" --outputFormat 2 --columns "key,summary,status"
Get issue details:
# Get full issue details
acli jira --action getIssue --issue PROJ-123
# Get specific fields
acli jira --action getIssue --issue PROJ-123 --outputFormat 2 --columns "key,summary,description,status,assignee"
Create issue:
# Create issue
acli jira --action createIssue \
--project PROJ \
--type "Story" \
--summary "Implement authentication" \
--description "Add OAuth2 authentication to the application" \
--priority "High" \
--labels "backend,security"
# Create with custom fields
acli jira --action createIssue \
--project PROJ \
--type "Bug" \
--summary "Login fails on mobile" \
--field "customfield_10001=High Priority"
Update issue:
# Update summary and description
acli jira --action updateIssue \
--issue PROJ-123 \
--summary "Updated summary" \
--description "Updated description"
# Update custom fields
acli jira --action updateIssue \
--issue PROJ-123 \
--field "customfield_10001=New Value"
# Add labels
acli jira --action updateIssue \
--issue PROJ-123 \
--labels "bug,urgent" \
--labelsAdd
Transition issue:
# Move to different status
acli jira --action transitionIssue \
--issue PROJ-123 \
--transition "In Progress"
# Transition with comment
acli jira --action transitionIssue \
--issue PROJ-123 \
--transition "Done" \
--comment "Completed implementation and testing"
Assign issue:
# Assign to user
acli jira --action assignIssue \
--issue PROJ-123 \
--assignee "john.doe"
# Assign to me
acli jira --action assignIssue \
--issue PROJ-123 \
--assignee "@me"
Sprint Operations
List sprints:
# List sprints for board
acli jira --action getSprintList \
--board "PROJ Board"
# List active sprints
acli jira --action getSprintList \
--board "PROJ Board" \
--state "active"
Add issues to sprint:
# Add single issue
acli jira --action addIssuesToSprint \
--sprint "Sprint 24" \
--issue "PROJ-123"
# Add multiple issues
acli jira --action addIssuesToSprint \
--sprint "Sprint 24" \
--issue "PROJ-123,PROJ-124,PROJ-125"
Start sprint:
# Start sprint with date range
acli jira --action startSprint \
--sprint "Sprint 24" \
--startDate "2024-01-01" \
--endDate "2024-01-14"
Close sprint:
# Complete sprint (moves incomplete issues to backlog)
acli jira --action completeSprint \
--sprint "Sprint 24"
Board Management
List boards:
# List all boards
acli jira --action getBoardList
# List boards for project
acli jira --action getBoardList \
--project PROJ
Get board configuration:
# Get board details
acli jira --action getBoard \
--board "PROJ Board"
Bulk Operations
Bulk transition:
# Transition multiple issues
acli jira --action progressIssue \
--issue "PROJ-123,PROJ-124,PROJ-125" \
--transition "In Progress"
Bulk update:
# Update multiple issues
acli jira --action updateIssue \
--issue "PROJ-123,PROJ-124" \
--labels "sprint-24" \
--labelsAdd
For complete acli command reference, see references/acli-reference.md.
Jira REST API Patterns
API Basics
import axios from 'axios';
// Configure API client
const jiraClient = axios.create({
baseURL: 'https://your-domain.atlassian.net/rest/api/3',
auth: {
username: 'user@example.com',
password: 'your-api-token'
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
Issue CRUD Operations
Get issue:
const response = await jiraClient.get(`/issue/PROJ-123`);
const issue = response.data;
console.log(issue.key);
console.log(issue.fields.summary);
console.log(issue.fields.status.name);
Create issue:
const newIssue = await jiraClient.post('/issue', {
fields: {
project: {
key: 'PROJ'
},
summary: 'Implement authentication',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Add OAuth2 authentication'
}
]
}
]
},
issuetype: {
name: 'Story'
},
priority: {
name: 'High'
},
labels: ['backend', 'security']
}
});
console.log(`Created issue: ${newIssue.data.key}`);
Update issue:
await jiraClient.put(`/issue/PROJ-123`, {
fields: {
summary: 'Updated summary',
labels: ['bug', 'urgent']
}
});
Transition issue:
// Get available transitions
const transitionsResp = await jiraClient.get(`/issue/PROJ-123/transitions`);
const transitions = transitionsResp.data.transitions;
// Find "In Progress" transition
const inProgressTransition = transitions.find(t => t.name === 'In Progress');
// Execute transition
await jiraClient.post(`/issue/PROJ-123/transitions`, {
transition: {
id: inProgressTransition.id
}
});
Advanced Operations
Add comment:
await jiraClient.post(`/issue/PROJ-123/comment`, {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This issue has been reviewed and approved'
}
]
}
]
}
});
Add attachment:
import FormData from 'form-data';
import fs from 'fs';
const form = new FormData();
form.append('file', fs.createReadStream('screenshot.png'));
await jiraClient.post(`/issue/PROJ-123/attachments`, form, {
headers: {
...form.getHeaders(),
'X-Atlassian-Token': 'no-check'
}
});
Link issues:
await jiraClient.post('/issueLink', {
type: {
name: 'Blocks'
},
inwardIssue: {
key: 'PROJ-123'
},
outwardIssue: {
key: 'PROJ-456'
}
});
For complete API patterns and examples, see references/jira-api-patterns.md.
JQL (Jira Query Language)
JQL is Jira's query language for searching and filtering issues.
Basic JQL Syntax
# Single condition
project = PROJ
# Multiple conditions (AND)
project = PROJ AND status = "To Do"
# Multiple conditions (OR)
status = "To Do" OR status = "In Progress"
# Negation
status != Done
# IN operator
status IN ("To Do", "In Progress")
# Comparison
created >= -7d
Common JQL Queries
By status:
# Open issues
status IN ("To Do", "In Progress", "Review")
# Closed issues
status = Done
# Not done
status != Done
By assignee:
# Assigned to me
assignee = currentUser()
# Unassigned
assignee IS EMPTY
# Assigned to specific user
assignee = "john.doe"
By date:
# Created in last 7 days
created >= -7d
# Updated today
updated >= startOfDay()
# Due this week
due <= endOfWeek()
By sprint:
# Current sprint
sprint in openSprints()
# Specific sprint
sprint = "Sprint 24"
# Issues not in sprint
sprint IS EMPTY
By label:
# Has specific label
labels = backend
# Has any of multiple labels
labels IN (backend, frontend)
# Missing labels
labels IS EMPTY
Advanced JQL Patterns
Combination queries:
# Sprint items assigned to me
project = PROJ AND sprint in openSprints() AND assignee = currentUser()
# High priority bugs
project = PROJ AND issuetype = Bug AND priority IN (Highest, High)
# Overdue items
duedate < now() AND status != Done
Using functions:
# Issues updated by me
updatedBy = currentUser()
# Issues where I'm a watcher
watcher = currentUser()
# Issues in epics
"Epic Link" IS NOT EMPTY
Ordering results:
# Order by priority, then created date
project = PROJ ORDER BY priority DESC, created ASC
# Multiple sort fields
status = "To Do" ORDER BY priority DESC, updated DESC
For 30+ JQL query examples, see examples/jql-query-examples.md.
ADF (Atlassian Document Format)
ADF is Jira's JSON-based format for rich text content in descriptions and comments.
Basic ADF Structure
// Simple text paragraph
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Hello, world!'
}
]
}
]
};
Text Formatting
Bold, italic, code:
{
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is ',
marks: []
},
{
type: 'text',
text: 'bold',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: ', '
},
{
type: 'text',
text: 'italic',
marks: [{ type: 'em' }]
},
{
type: 'text',
text: ', and '
},
{
type: 'text',
text: 'code',
marks: [{ type: 'code' }]
}
]
}
]
}
Links
{
type: 'text',
text: 'Click here',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com'
}
}
]
}
Code Blocks
{
type: 'codeBlock',
attrs: {
language: 'typescript'
},
content: [
{
type: 'text',
text: 'function hello() {\n console.log("Hello");\n}'
}
]
}
Lists
Bullet list:
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First item'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second item'
}
]
}
]
}
]
}
Ordered list:
{
type: 'orderedList',
content: [
// Same listItem structure as bulletList
]
}
Helper Functions
// Create simple text paragraph
function createParagraph(text: string) {
return {
type: 'paragraph',
content: [
{
type: 'text',
text
}
]
};
}
// Create ADF document
function createADFDocument(...paragraphs: any[]) {
return {
type: 'doc',
version: 1,
content: paragraphs
};
}
// Usage
const doc = createADFDocument(
createParagraph('First paragraph'),
createParagraph('Second paragraph')
);
For complete ADF specification and templates, see references/adf-format-guide.md and examples/adf-comment-templates.md.
Issue Detection and Parsing
Jira URL Patterns
// Jira issue URL pattern
const JIRA_ISSUE_URL = /https?:\/\/([^\/]+)\.atlassian\.net\/browse\/([A-Z]+-\d+)/g;
// Custom Jira domain
const JIRA_CUSTOM_URL = /https?:\/\/jira\.([^\/]+)\.com\/browse\/([A-Z]+-\d+)/g;
function detectJiraIssues(text: string) {
const matches = Array.from(text.matchAll(JIRA_ISSUE_URL));
return matches.map(match => ({
url: match[0],
domain: match[1],
key: match[2]
}));
}
// Example
const text = "See https://mycompany.atlassian.net/browse/PROJ-123";
const issues = detectJiraIssues(text);
// => [{ url: "...", domain: "mycompany", key: "PROJ-123" }]
Jira Issue Key Pattern
// Issue key pattern (e.g., PROJ-123)
const JIRA_KEY = /\b([A-Z]{2,10}-\d+)\b/g;
function extractJiraKeys(text: string): string[] {
const matches = Array.from(text.matchAll(JIRA_KEY));
return matches.map(m => m[1]);
}
// Example
const text = "Implements PROJ-123 and fixes PROJ-456";
const keys = extractJiraKeys(text);
// => ["PROJ-123", "PROJ-456"]
Auto-fetch Issue Details
async function fetchJiraIssue(key: string) {
const response = await jiraClient.get(`/issue/${key}`);
return {
key: response.data.key,
summary: response.data.fields.summary,
status: response.data.fields.status.name,
assignee: response.data.fields.assignee?.displayName,
url: `https://your-domain.atlassian.net/browse/${key}`
};
}
// Auto-enrich text with issue details
async function enrichWithJiraData(text: string) {
const keys = extractJiraKeys(text);
const issues = await Promise.all(keys.map(fetchJiraIssue));
let enriched = text;
issues.forEach(issue => {
const pattern = new RegExp(issue.key, 'g');
enriched = enriched.replace(
pattern,
`[${issue.key}](${issue.url}) (${issue.summary})`
);
});
return enriched;
}
Sprint Management
Sprint Planning Workflow
# 1. Create new sprint
acli jira --action createSprint \
--board "PROJ Board" \
--name "Sprint 25" \
--startDate "2024-01-15" \
--endDate "2024-01-28"
# 2. Add issues to sprint (from JQL query)
acli jira --action getIssueList \
--jql "project = PROJ AND labels = 'sprint-ready'" \
--outputFormat 999 | \
acli jira --action addIssuesToSprint \
--sprint "Sprint 25" \
--issue "@-"
# 3. Start sprint
acli jira --action startSprint \
--sprint "Sprint 25"
Sprint Reporting
interface SprintMetrics {
name: string;
total: number;
completed: number;
inProgress: number;
todo: number;
velocity: number;
}
async function getSprintMetrics(sprintId: string): Promise<SprintMetrics> {
const response = await jiraClient.get(`/sprint/${sprintId}/issues`);
const issues = response.data.issues;
const completed = issues.filter((i: any) => i.fields.status.name === 'Done').length;
const inProgress = issues.filter((i: any) => i.fields.status.name === 'In Progress').length;
const todo = issues.filter((i: any) => i.fields.status.name === 'To Do').length;
return {
name: response.data.sprint.name,
total: issues.length,
completed,
inProgress,
todo,
velocity: (completed / issues.length) * 100
};
}
Quick Reference
Essential acli Commands
acli jira --action getIssueList --jql "query"- Query issuesacli jira --action createIssue --project PROJ --type Story --summary "..."- Create issueacli jira --action transitionIssue --issue KEY --transition "Status"- Change statusacli jira --action addIssuesToSprint --sprint "Sprint" --issue "KEY"- Add to sprintacli jira --action getSprintList --board "Board"- List sprints
Key API Endpoints
GET /issue/{issueKey}- Get issuePOST /issue- Create issuePUT /issue/{issueKey}- Update issuePOST /issue/{issueKey}/transitions- Transition issuePOST /issue/{issueKey}/comment- Add comment
JQL Quick Patterns
project = PROJ AND assignee = currentUser()- My issuessprint in openSprints()- Current sprintstatus = "To Do" ORDER BY priority DESC- Prioritized backlogcreated >= -7d- Recent issues
ADF Basics
- Paragraph:
{type: 'paragraph', content: [{type: 'text', text: '...'}]} - Bold:
marks: [{type: 'strong'}] - Code:
marks: [{type: 'code'}] - Link:
marks: [{type: 'link', attrs: {href: '...'}}]
Best Practices
- Always use API tokens (not passwords) for authentication
- Cache Jira project metadata to reduce API calls
- Use JQL for complex queries instead of filtering in code
- Validate issue keys before API calls (format: PROJECT-123)
- Use ADF for all rich text content (descriptions, comments)
- Handle Jira API rate limits (10 requests/second)
Multi-Specialist ADF Comment Support
When working with multi-specialist implementations, Jira comments need to aggregate work from multiple specialists into a single, well-formatted comment.
Detection Logic
Check for multi-specialist mode:
# Detect if this is a multi-specialist implementation
FEATURE_NAME="authentication" # Extract from issue or context
if [ -d ".agency/handoff/${FEATURE_NAME}" ]; then
echo "Multi-specialist mode detected"
MODE="multi-specialist"
else
echo "Single-specialist mode"
MODE="single-specialist"
fi
Gather specialist information:
# List all specialists who worked on this feature
if [ -d ".agency/handoff/${FEATURE_NAME}" ]; then
specialists=$(ls -d .agency/handoff/${FEATURE_NAME}/*/ | xargs -n1 basename)
for specialist in $specialists; do
echo "Found specialist: $specialist"
# Read specialist's summary
if [ -f ".agency/handoff/${FEATURE_NAME}/${specialist}/summary.md" ]; then
cat ".agency/handoff/${FEATURE_NAME}/${specialist}/summary.md"
fi
# Read specialist's verification
if [ -f ".agency/handoff/${FEATURE_NAME}/${specialist}/verification.md" ]; then
cat ".agency/handoff/${FEATURE_NAME}/${specialist}/verification.md"
fi
done
fi
Multi-Specialist ADF Comment Template
Complete example with multiple specialists:
interface SpecialistWork {
name: string;
displayName: string;
summary: string;
filesChanged: string[];
testResults: string;
status: 'success' | 'warning' | 'error';
}
function createMultiSpecialistComment(
featureName: string,
specialists: SpecialistWork[],
overallStatus: 'success' | 'warning' | 'error',
integrationPoints: string[]
): object {
const statusEmoji = {
success: '✅',
warning: '⚠️',
error: '❌'
};
const panelType = {
success: 'success',
warning: 'warning',
error: 'error'
};
return {
version: 1,
type: 'doc',
content: [
// Header panel with overall status
{
type: 'panel',
attrs: {
panelType: panelType[overallStatus]
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: `${statusEmoji[overallStatus]} Multi-Specialist Implementation Complete`,
marks: [{ type: 'strong' }]
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: `Feature: ${featureName} | Specialists: ${specialists.length}`
}
]
}
]
},
// Specialists summary
{
type: 'heading',
attrs: { level: 3 },
content: [
{
type: 'text',
text: 'Specialist Contributions'
}
]
},
// List of specialists with status
{
type: 'bulletList',
content: specialists.map(specialist => ({
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: `${statusEmoji[specialist.status]} `,
marks: []
},
{
type: 'text',
text: specialist.displayName,
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: ` - ${specialist.summary}`
}
]
}
]
}))
},
// Detailed work by specialist (collapsible-like sections)
{
type: 'heading',
attrs: { level: 3 },
content: [
{
type: 'text',
text: 'Detailed Work Breakdown'
}
]
},
...specialists.flatMap(specialist => [
// Specialist heading
{
type: 'heading',
attrs: { level: 4 },
content: [
{
type: 'text',
text: `${specialist.displayName} ${statusEmoji[specialist.status]}`
}
]
},
// Summary
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Summary: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: specialist.summary
}
]
},
// Files changed
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Files Changed: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: `${specialist.filesChanged.length} files`
}
]
},
{
type: 'bulletList',
content: specialist.filesChanged.slice(0, 10).map(file => ({
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: file,
marks: [{ type: 'code' }]
}
]
}
]
}))
},
// Test results
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Tests: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: specialist.testResults
}
]
}
]),
// Integration points
{
type: 'heading',
attrs: { level: 3 },
content: [
{
type: 'text',
text: 'Integration Points'
}
]
},
{
type: 'bulletList',
content: integrationPoints.map(point => ({
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: point
}
]
}
]
}))
}
]
};
}
Usage example:
const specialists: SpecialistWork[] = [
{
name: 'backend-architect',
displayName: 'Backend Architect',
summary: 'Implemented authentication API with JWT and refresh tokens',
filesChanged: [
'src/api/auth/login.ts',
'src/api/auth/refresh.ts',
'src/middleware/authenticate.ts',
'src/models/user.ts'
],
testResults: 'All tests passing (24/24)',
status: 'success'
},
{
name: 'frontend-developer',
displayName: 'Frontend Developer',
summary: 'Created login/signup forms and integrated with auth API',
filesChanged: [
'src/components/LoginForm.tsx',
'src/components/SignupForm.tsx',
'src/hooks/useAuth.ts',
'src/pages/profile.tsx'
],
testResults: 'All tests passing (18/18)',
status: 'success'
}
];
const comment = createMultiSpecialistComment(
'Authentication System',
specialists,
'success',
[
'Backend exposes /api/auth/login and /api/auth/refresh endpoints',
'Frontend uses useAuth hook to manage authentication state',
'JWT tokens stored in httpOnly cookies',
'Protected routes redirect to login when unauthenticated'
]
);
// Post to Jira
await jiraClient.post(`/issue/PROJ-123/comment`, { body: comment });
Single-Specialist ADF Comment Template
Backward compatibility - Keep existing single-specialist format:
function createSingleSpecialistComment(
summary: string,
filesChanged: string[],
testResults: string,
status: 'success' | 'warning' | 'error'
): object {
const statusEmoji = {
success: '✅',
warning: '⚠️',
error: '❌'
};
const panelType = {
success: 'success',
warning: 'warning',
error: 'error'
};
return {
version: 1,
type: 'doc',
content: [
{
type: 'panel',
attrs: {
panelType: panelType[status]
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: `${statusEmoji[status]} Implementation Complete`,
marks: [{ type: 'strong' }]
}
]
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Summary: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: summary
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Files Changed: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: `${filesChanged.length} files`
}
]
},
{
type: 'bulletList',
content: filesChanged.slice(0, 10).map(file => ({
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: file,
marks: [{ type: 'code' }]
}
]
}
]
}))
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Tests: ',
marks: [{ type: 'strong' }]
},
{
type: 'text',
text: testResults
}
]
}
]
};
}
Auto-Detection Wrapper
Automatically choose the right format:
async function postImplementationComment(
issueKey: string,
featureName: string
): Promise<void> {
const handoffDir = `.agency/handoff/${featureName}`;
// Check if multi-specialist mode
if (fs.existsSync(handoffDir)) {
// Multi-specialist mode
const specialists: SpecialistWork[] = [];
const specialistDirs = fs.readdirSync(handoffDir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const specialistName of specialistDirs) {
const summaryPath = `${handoffDir}/${specialistName}/summary.md`;
const verificationPath = `${handoffDir}/${specialistName}/verification.md`;
if (fs.existsSync(summaryPath)) {
const summary = fs.readFileSync(summaryPath, 'utf-8');
const verification = fs.existsSync(verificationPath)
? fs.readFileSync(verificationPath, 'utf-8')
: '';
// Parse summary and verification to extract data
const specialist = parseSpecialistData(specialistName, summary, verification);
specialists.push(specialist);
}
}
// Determine overall status
const overallStatus = specialists.every(s => s.status === 'success')
? 'success'
: specialists.some(s => s.status === 'error')
? 'error'
: 'warning';
// Extract integration points from summaries
const integrationPoints = extractIntegrationPoints(specialists);
const comment = createMultiSpecialistComment(
featureName,
specialists,
overallStatus,
integrationPoints
);
await jiraClient.post(`/issue/${issueKey}/comment`, { body: comment });
} else {
// Single-specialist mode (backward compatible)
const summary = 'Implementation completed';
const filesChanged = await getChangedFiles();
const testResults = 'All tests passing';
const status = 'success';
const comment = createSingleSpecialistComment(
summary,
filesChanged,
testResults,
status
);
await jiraClient.post(`/issue/${issueKey}/comment`, { body: comment });
}
}
Helper Functions
function parseSpecialistData(
name: string,
summary: string,
verification: string
): SpecialistWork {
// Extract display name
const displayNames: Record<string, string> = {
'backend-architect': 'Backend Architect',
'frontend-developer': 'Frontend Developer',
'database-specialist': 'Database Specialist',
'devops-engineer': 'DevOps Engineer'
};
// Extract summary (first paragraph or heading)
const summaryMatch = summary.match(/^##?\s+(.+)$/m) ||
summary.match(/^(.+)$/m);
const summaryText = summaryMatch ? summaryMatch[1] : 'Work completed';
// Extract files from summary (look for code blocks or lists)
const filesMatch = summary.match(/```[^`]*```/s) ||
summary.match(/^[-*]\s+`([^`]+)`/gm);
const filesChanged = filesMatch
? Array.from(summary.matchAll(/`([^`]+\.[a-z]+)`/g)).map(m => m[1])
: [];
// Extract test results
const testMatch = verification.match(/Tests?:\s*(.+)/i) ||
verification.match(/(\d+\/\d+\s+passing)/i);
const testResults = testMatch ? testMatch[1] : 'Tests completed';
// Determine status from verification
let status: 'success' | 'warning' | 'error' = 'success';
if (verification.includes('❌') || verification.includes('FAIL')) {
status = 'error';
} else if (verification.includes('⚠️') || verification.includes('WARNING')) {
status = 'warning';
}
return {
name,
displayName: displayNames[name] || name,
summary: summaryText,
filesChanged,
testResults,
status
};
}
function extractIntegrationPoints(specialists: SpecialistWork[]): string[] {
const points: string[] = [];
// Look for API endpoints from backend
const backend = specialists.find(s => s.name === 'backend-architect');
if (backend) {
const apiMatches = backend.summary.match(/\/api\/[^\s]+/g);
if (apiMatches) {
points.push(...apiMatches.map(api => `Backend exposes ${api} endpoint`));
}
}
// Look for components from frontend
const frontend = specialists.find(s => s.name === 'frontend-developer');
if (frontend) {
const componentMatches = frontend.filesChanged
.filter(f => f.endsWith('.tsx') || f.endsWith('.jsx'));
if (componentMatches.length > 0) {
points.push(`Frontend components: ${componentMatches.join(', ')}`);
}
}
return points.length > 0 ? points : ['See individual specialist sections for details'];
}
async function getChangedFiles(): Promise<string[]> {
// Get changed files from git using execFile for security
const { execFile } = require('child_process').promises;
try {
const { stdout } = await execFile('git', ['diff', '--name-only', 'HEAD']);
return stdout.trim().split('\n').filter(Boolean);
} catch (error) {
console.error('Failed to get changed files:', error);
return [];
}
}
Best Practices
Multi-specialist comments:
- Always include overall status panel at top
- List all specialists with emoji status indicators
- Provide detailed breakdown per specialist
- Highlight integration points between specialists
- Keep file lists reasonable (max 10 per specialist)
- Include test results for each specialist
- Use consistent formatting across all specialists
ADF structure:
- Use panels for status (success/warning/error)
- Use headings (level 3-4) for major sections
- Use bullet lists for file listings
- Use code marks for file paths and code references
- Keep paragraphs focused and concise
- Always validate ADF structure before posting
Detection logic:
- Always check for
.agency/handoff/{feature}directory first - Fall back to single-specialist format if not found
- Handle missing files gracefully (summary.md, verification.md)
- Parse specialist data defensively with defaults
- Validate all parsed data before creating ADF
Related Skills
- jira-adf-generator: Generate properly formatted ADF for Jira comments
- github-integration: Alternative provider integration for comparison
- agency-workflow-patterns: General workflow automation applicable to any provider
- github-workflow-best-practices: Sprint concepts and planning patterns