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.
Automates Jira project management using the acli CLI and REST API for issue creation, sprint operations, and JQL queries. Use it when you need to programmatically manage Jira workflows, update issues, or query project data.
/plugin marketplace add squirrelsoft-dev/agency/plugin install agency@squirrelsoft-dev-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/acli-scripts.mdexamples/adf-comment-templates.mdexamples/jql-query-examples.mdreferences/acli-reference.mdreferences/adf-format-guide.mdreferences/jira-api-patterns.mdThis 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.
The Atlassian CLI (acli) is the primary tool for Jira automation via command line.
# 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
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"
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"
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 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.
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'
}
});
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
}
});
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 is Jira's query language for searching and filtering issues.
# 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
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
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 is Jira's JSON-based format for rich text content in descriptions and comments.
// Simple text paragraph
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Hello, world!'
}
]
}
]
};
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' }]
}
]
}
]
}
{
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}'
}
]
}
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
]
}
// 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.
// 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" - 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 sprintsGET /issue/{issueKey} - Get issuePOST /issue - Create issuePUT /issue/{issueKey} - Update issuePOST /issue/{issueKey}/transitions - Transition issuePOST /issue/{issueKey}/comment - Add commentproject = PROJ AND assignee = currentUser() - My issuessprint in openSprints() - Current sprintstatus = "To Do" ORDER BY priority DESC - Prioritized backlogcreated >= -7d - Recent issues{type: 'paragraph', content: [{type: 'text', text: '...'}]}marks: [{type: 'strong'}]marks: [{type: 'code'}]marks: [{type: 'link', attrs: {href: '...'}}]When working with multi-specialist implementations, Jira comments need to aggregate work from multiple specialists into a single, well-formatted comment.
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
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 });
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
}
]
}
]
};
}
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 });
}
}
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 [];
}
}
Multi-specialist comments:
ADF structure:
Detection logic:
.agency/handoff/{feature} directory firstThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.