Loading...
Loading...
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.
npx skill4agent add squirrelsoft-dev/agency jira-integrationacli# 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~/.acli/acli.propertiesserver=https://your-domain.atlassian.net
user=user@example.com
password=your-api-token# 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 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
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 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# 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 to user
acli jira --action assignIssue \
--issue PROJ-123 \
--assignee "john.doe"
# Assign to me
acli jira --action assignIssue \
--issue PROJ-123 \
--assignee "@me"# List sprints for board
acli jira --action getSprintList \
--board "PROJ Board"
# List active sprints
acli jira --action getSprintList \
--board "PROJ Board" \
--state "active"# 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 with date range
acli jira --action startSprint \
--sprint "Sprint 24" \
--startDate "2024-01-01" \
--endDate "2024-01-14"# Complete sprint (moves incomplete issues to backlog)
acli jira --action completeSprint \
--sprint "Sprint 24"# List all boards
acli jira --action getBoardList
# List boards for project
acli jira --action getBoardList \
--project PROJ# Get board details
acli jira --action getBoard \
--board "PROJ Board"# Transition multiple issues
acli jira --action progressIssue \
--issue "PROJ-123,PROJ-124,PROJ-125" \
--transition "In Progress"# Update multiple issues
acli jira --action updateIssue \
--issue "PROJ-123,PROJ-124" \
--labels "sprint-24" \
--labelsAddreferences/acli-reference.mdimport 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'
}
});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);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}`);await jiraClient.put(`/issue/PROJ-123`, {
fields: {
summary: 'Updated summary',
labels: ['bug', 'urgent']
}
});// 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
}
});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'
}
]
}
]
}
});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'
}
});await jiraClient.post('/issueLink', {
type: {
name: 'Blocks'
},
inwardIssue: {
key: 'PROJ-123'
},
outwardIssue: {
key: 'PROJ-456'
}
});references/jira-api-patterns.md# 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# Open issues
status IN ("To Do", "In Progress", "Review")
# Closed issues
status = Done
# Not done
status != Done# Assigned to me
assignee = currentUser()
# Unassigned
assignee IS EMPTY
# Assigned to specific user
assignee = "john.doe"# Created in last 7 days
created >= -7d
# Updated today
updated >= startOfDay()
# Due this week
due <= endOfWeek()# Current sprint
sprint in openSprints()
# Specific sprint
sprint = "Sprint 24"
# Issues not in sprint
sprint IS EMPTY# Has specific label
labels = backend
# Has any of multiple labels
labels IN (backend, frontend)
# Missing labels
labels IS EMPTY# 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# Issues updated by me
updatedBy = currentUser()
# Issues where I'm a watcher
watcher = currentUser()
# Issues in epics
"Epic Link" IS NOT EMPTY# 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 DESCexamples/jql-query-examples.md// Simple text paragraph
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Hello, world!'
}
]
}
]
};{
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' }]
}
]
}
]
}{
type: 'text',
text: 'Click here',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com'
}
}
]
}{
type: 'codeBlock',
attrs: {
language: 'typescript'
},
content: [
{
type: 'text',
text: 'function hello() {\n console.log("Hello");\n}'
}
]
}{
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'
}
]
}
]
}
]
}{
type: 'orderedList',
content: [
// Same listItem structure as bulletList
]
}// 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')
);references/adf-format-guide.mdexamples/adf-comment-templates.md// 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" }]// 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"]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;
}# 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"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
};
}acli jira --action getIssueList --jql "query"acli jira --action createIssue --project PROJ --type Story --summary "..."acli jira --action transitionIssue --issue KEY --transition "Status"acli jira --action addIssuesToSprint --sprint "Sprint" --issue "KEY"acli jira --action getSprintList --board "Board"GET /issue/{issueKey}POST /issuePUT /issue/{issueKey}POST /issue/{issueKey}/transitionsPOST /issue/{issueKey}/commentproject = PROJ AND assignee = currentUser()sprint in openSprints()status = "To Do" ORDER BY priority DESCcreated >= -7d{type: 'paragraph', content: [{type: 'text', text: '...'}]}marks: [{type: 'strong'}]marks: [{type: 'code'}]marks: [{type: 'link', attrs: {href: '...'}}]# 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# 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
fiinterface 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
}
]
}
]
}))
}
]
};
}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 });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
}
]
}
]
};
}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 });
}
}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 [];
}
}.agency/handoff/{feature}