jira-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Jira Integration Mastery

Jira集成精通指南

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.
本技能提供使用Atlassian CLI(acli)、Jira REST API和ADF(Atlassian文档格式)进行Jira集成的全面指导,是自动化问题管理、Sprint规划、JQL查询以及构建可靠的Jira工作流的必备内容。

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

  • 自动化Jira问题的创建、更新和状态流转
  • 构建Sprint规划工作流
  • 编写并执行JQL查询
  • 使用ADF(Atlassian文档格式)格式化评论
  • 解析Jira URL并提取元数据
  • 将Jira集成到开发自动化流程中
  • 对多个问题执行批量操作

Atlassian CLI (acli) Mastery

Atlassian CLI(acli)精通指南

The Atlassian CLI (
acli
) is the primary tool for Jira automation via command line.
Atlassian CLI(
acli
)是通过命令行实现Jira自动化的主要工具。

Installation and Configuration

安装与配置

bash
undefined
bash
undefined

Download acli

下载acli

Extract and setup

解压并设置

unzip acli-9.8.0-distribution.zip export PATH=$PATH:/path/to/acli
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
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/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`):
```properties
server=https://your-domain.atlassian.net
user=user@example.com
password=your-api-token
acli jira --server https://your-domain.atlassian.net --user user@example.com --password your-api-token --action login

**配置文件**(`~/.acli/acli.properties`):
```properties
server=https://your-domain.atlassian.net
user=user@example.com
password=your-api-token

Issue Operations

问题操作

List issues:
bash
undefined
列出问题:
bash
undefined

List issues in project

列出项目中的问题

acli jira --action getIssueList --project PROJ
acli jira --action getIssueList --project PROJ

List with JQL

使用JQL列出问题

acli jira --action getIssueList --jql "project = PROJ AND status = 'To Do'"
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**:
```bash
acli jira --action getIssueList --jql "assignee = currentUser()" --outputFormat 2 --columns "key,summary,status"

**获取问题详情**:
```bash

Get full issue details

获取完整的问题详情

acli jira --action getIssue --issue PROJ-123
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**:
```bash
acli jira --action getIssue --issue PROJ-123 --outputFormat 2 --columns "key,summary,description,status,assignee"

**创建问题**:
```bash

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"
acli jira --action createIssue
--project PROJ
--type "Story"
--summary "实现认证功能"
--description "为应用添加OAuth2认证"
--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**:
```bash
acli jira --action createIssue
--project PROJ
--type "Bug"
--summary "移动端登录失败"
--field "customfield_10001=High Priority"

**更新问题**:
```bash

Update summary and description

更新摘要和描述

acli jira --action updateIssue
--issue PROJ-123
--summary "Updated summary"
--description "Updated description"
acli jira --action updateIssue
--issue PROJ-123
--summary "更新后的摘要"
--description "更新后的描述"

Update custom fields

更新自定义字段

acli jira --action updateIssue
--issue PROJ-123
--field "customfield_10001=New Value"
acli jira --action updateIssue
--issue PROJ-123
--field "customfield_10001=新值"

Add labels

添加标签

acli jira --action updateIssue
--issue PROJ-123
--labels "bug,urgent"
--labelsAdd

**Transition issue**:
```bash
acli jira --action updateIssue
--issue PROJ-123
--labels "bug,urgent"
--labelsAdd

**流转问题状态**:
```bash

Move to different status

切换到不同状态

acli jira --action transitionIssue
--issue PROJ-123
--transition "In Progress"
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**:
```bash
acli jira --action transitionIssue
--issue PROJ-123
--transition "Done"
--comment "已完成开发和测试"

**分配问题**:
```bash

Assign to user

分配给指定用户

acli jira --action assignIssue
--issue PROJ-123
--assignee "john.doe"
acli jira --action assignIssue
--issue PROJ-123
--assignee "john.doe"

Assign to me

分配给自己

acli jira --action assignIssue
--issue PROJ-123
--assignee "@me"
undefined
acli jira --action assignIssue
--issue PROJ-123
--assignee "@me"
undefined

Sprint Operations

Sprint操作

List sprints:
bash
undefined
列出Sprint:
bash
undefined

List sprints for board

列出看板的Sprint

acli jira --action getSprintList
--board "PROJ Board"
acli jira --action getSprintList
--board "PROJ Board"

List active sprints

列出活跃Sprint

acli jira --action getSprintList
--board "PROJ Board"
--state "active"

**Add issues to sprint**:
```bash
acli jira --action getSprintList
--board "PROJ Board"
--state "active"

**添加问题到Sprint**:
```bash

Add single issue

添加单个问题

acli jira --action addIssuesToSprint
--sprint "Sprint 24"
--issue "PROJ-123"
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**:
```bash
acli jira --action addIssuesToSprint
--sprint "Sprint 24"
--issue "PROJ-123,PROJ-124,PROJ-125"

**启动Sprint**:
```bash

Start sprint with date range

指定日期范围启动Sprint

acli jira --action startSprint
--sprint "Sprint 24"
--startDate "2024-01-01"
--endDate "2024-01-14"

**Close sprint**:
```bash
acli jira --action startSprint
--sprint "Sprint 24"
--startDate "2024-01-01"
--endDate "2024-01-14"

**关闭Sprint**:
```bash

Complete sprint (moves incomplete issues to backlog)

完成Sprint(将未完成问题移至待办)

acli jira --action completeSprint
--sprint "Sprint 24"
undefined
acli jira --action completeSprint
--sprint "Sprint 24"
undefined

Board Management

看板管理

List boards:
bash
undefined
列出看板:
bash
undefined

List all boards

列出所有看板

acli jira --action getBoardList
acli jira --action getBoardList

List boards for project

列出项目的看板

acli jira --action getBoardList
--project PROJ

**Get board configuration**:
```bash
acli jira --action getBoardList
--project PROJ

**获取看板配置**:
```bash

Get board details

获取看板详情

acli jira --action getBoard
--board "PROJ Board"
undefined
acli jira --action getBoard
--board "PROJ Board"
undefined

Bulk Operations

批量操作

Bulk transition:
bash
undefined
批量流转状态:
bash
undefined

Transition multiple issues

批量流转问题状态

acli jira --action progressIssue
--issue "PROJ-123,PROJ-124,PROJ-125"
--transition "In Progress"

**Bulk update**:
```bash
acli jira --action progressIssue
--issue "PROJ-123,PROJ-124,PROJ-125"
--transition "In Progress"

**批量更新**:
```bash

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`.

---
acli jira --action updateIssue
--issue "PROJ-123,PROJ-124"
--labels "sprint-24"
--labelsAdd

完整的acli命令参考,请查看`references/acli-reference.md`。

---

Jira REST API Patterns

Jira REST API模式

API Basics

API基础

typescript
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'
  }
});
typescript
import axios from 'axios';

// 配置API客户端
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

问题CRUD操作

Get issue:
typescript
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:
typescript
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:
typescript
await jiraClient.put(`/issue/PROJ-123`, {
  fields: {
    summary: 'Updated summary',
    labels: ['bug', 'urgent']
  }
});
Transition issue:
typescript
// 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
  }
});
获取问题:
typescript
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);
创建问题:
typescript
const newIssue = await jiraClient.post('/issue', {
  fields: {
    project: {
      key: 'PROJ'
    },
    summary: '实现认证功能',
    description: {
      type: 'doc',
      version: 1,
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '添加OAuth2认证'
            }
          ]
        }
      ]
    },
    issuetype: {
      name: 'Story'
    },
    priority: {
      name: 'High'
    },
    labels: ['backend', 'security']
  }
});

console.log(`已创建问题: ${newIssue.data.key}`);
更新问题:
typescript
await jiraClient.put(`/issue/PROJ-123`, {
  fields: {
    summary: '更新后的摘要',
    labels: ['bug', 'urgent']
  }
});
流转问题状态:
typescript
// 获取可用的状态流转
const transitionsResp = await jiraClient.get(`/issue/PROJ-123/transitions`);
const transitions = transitionsResp.data.transitions;

// 找到"In Progress"状态流转
const inProgressTransition = transitions.find(t => t.name === 'In Progress');

// 执行状态流转
await jiraClient.post(`/issue/PROJ-123/transitions`, {
  transition: {
    id: inProgressTransition.id
  }
});

Advanced Operations

高级操作

Add comment:
typescript
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:
typescript
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:
typescript
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
.

添加评论:
typescript
await jiraClient.post(`/issue/PROJ-123/comment`, {
  body: {
    type: 'doc',
    version: 1,
    content: [
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '该问题已审核通过'
          }
        ]
      }
    ]
  }
});
添加附件:
typescript
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'
  }
});
关联问题:
typescript
await jiraClient.post('/issueLink', {
  type: {
    name: 'Blocks'
  },
  inwardIssue: {
    key: 'PROJ-123'
  },
  outwardIssue: {
    key: 'PROJ-456'
  }
});
完整的API模式和示例,请查看
references/jira-api-patterns.md

JQL (Jira Query Language)

JQL(Jira查询语言)

JQL is Jira's query language for searching and filtering issues.
JQL是Jira用于搜索和筛选问题的查询语言。

Basic JQL Syntax

基础JQL语法

jql
undefined
jql
undefined

Single condition

单个条件

project = PROJ
project = PROJ

Multiple conditions (AND)

多个条件(AND)

project = PROJ AND status = "To Do"
project = PROJ AND status = "To Do"

Multiple conditions (OR)

多个条件(OR)

status = "To Do" OR status = "In Progress"
status = "To Do" OR status = "In Progress"

Negation

取反

status != Done
status != Done

IN operator

IN操作符

status IN ("To Do", "In Progress")
status IN ("To Do", "In Progress")

Comparison

比较

created >= -7d
undefined
created >= -7d
undefined

Common JQL Queries

常用JQL查询

By status:
jql
undefined
按状态:
jql
undefined

Open issues

未关闭的问题

status IN ("To Do", "In Progress", "Review")
status IN ("To Do", "In Progress", "Review")

Closed issues

已关闭的问题

status = Done
status = Done

Not done

未完成的问题

status != Done

**By assignee**:
```jql
status != Done

**按经办人**:
```jql

Assigned to me

我的问题

assignee = currentUser()
assignee = currentUser()

Unassigned

未分配的问题

assignee IS EMPTY
assignee IS EMPTY

Assigned to specific user

指定用户的问题

assignee = "john.doe"

**By date**:
```jql
assignee = "john.doe"

**按日期**:
```jql

Created in last 7 days

近7天创建的问题

created >= -7d
created >= -7d

Updated today

今天更新的问题

updated >= startOfDay()
updated >= startOfDay()

Due this week

本周到期的问题

due <= endOfWeek()

**By sprint**:
```jql
due <= endOfWeek()

**按Sprint**:
```jql

Current sprint

当前Sprint的问题

sprint in openSprints()
sprint in openSprints()

Specific sprint

指定Sprint的问题

sprint = "Sprint 24"
sprint = "Sprint 24"

Issues not in sprint

未加入Sprint的问题

sprint IS EMPTY

**By label**:
```jql
sprint IS EMPTY

**按标签**:
```jql

Has specific label

带指定标签的问题

labels = backend
labels = backend

Has any of multiple labels

带多个标签中任意一个的问题

labels IN (backend, frontend)
labels IN (backend, frontend)

Missing labels

无标签的问题

labels IS EMPTY
undefined
labels IS EMPTY
undefined

Advanced JQL Patterns

高级JQL模式

Combination queries:
jql
undefined
组合查询:
jql
undefined

Sprint items assigned to me

当前Sprint中我的问题

project = PROJ AND sprint in openSprints() AND assignee = currentUser()
project = PROJ AND sprint in openSprints() AND assignee = currentUser()

High priority bugs

高优先级Bug

project = PROJ AND issuetype = Bug AND priority IN (Highest, High)
project = PROJ AND issuetype = Bug AND priority IN (Highest, High)

Overdue items

逾期问题

duedate < now() AND status != Done

**Using functions**:
```jql
duedate < now() AND status != Done

**使用函数**:
```jql

Issues updated by me

我更新过的问题

updatedBy = currentUser()
updatedBy = currentUser()

Issues where I'm a watcher

我关注的问题

watcher = currentUser()
watcher = currentUser()

Issues in epics

属于史诗的问题

"Epic Link" IS NOT EMPTY

**Ordering results**:
```jql
"Epic Link" IS NOT EMPTY

**结果排序**:
```jql

Order by priority, then created date

按优先级降序、创建日期升序排序

project = PROJ ORDER BY priority DESC, created ASC
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`.

---
status = "To Do" ORDER BY priority DESC, updated DESC

30+个JQL查询示例,请查看`examples/jql-query-examples.md`。

---

ADF (Atlassian Document Format)

ADF(Atlassian文档格式)

ADF is Jira's JSON-based format for rich text content in descriptions and comments.
ADF是Jira用于描述和评论中富文本内容的JSON格式。

Basic ADF Structure

基础ADF结构

typescript
// Simple text paragraph
const adf = {
  type: 'doc',
  version: 1,
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'Hello, world!'
        }
      ]
    }
  ]
};
typescript
// 简单文本段落
const adf = {
  type: 'doc',
  version: 1,
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: '你好,世界!'
        }
      ]
    }
  ]
};

Text Formatting

文本格式化

Bold, italic, code:
typescript
{
  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' }]
        }
      ]
    }
  ]
}
加粗、斜体、代码:
typescript
{
  type: 'doc',
  version: 1,
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: '这是 ',
          marks: []
        },
        {
          type: 'text',
          text: '加粗',
          marks: [{ type: 'strong' }]
        },
        {
          type: 'text',
          text: ','
        },
        {
          type: 'text',
          text: '斜体',
          marks: [{ type: 'em' }]
        },
        {
          type: 'text',
          text: ',和 '
        },
        {
          type: 'text',
          text: '代码',
          marks: [{ type: 'code' }]
        }
      ]
    }
  ]
}

Links

链接

typescript
{
  type: 'text',
  text: 'Click here',
  marks: [
    {
      type: 'link',
      attrs: {
        href: 'https://example.com'
      }
    }
  ]
}
typescript
{
  type: 'text',
  text: '点击这里',
  marks: [
    {
      type: 'link',
      attrs: {
        href: 'https://example.com'
      }
    }
  ]
}

Code Blocks

代码块

typescript
{
  type: 'codeBlock',
  attrs: {
    language: 'typescript'
  },
  content: [
    {
      type: 'text',
      text: 'function hello() {\n  console.log("Hello");\n}'
    }
  ]
}
typescript
{
  type: 'codeBlock',
  attrs: {
    language: 'typescript'
  },
  content: [
    {
      type: 'text',
      text: 'function hello() {\n  console.log("Hello");\n}'
    }
  ]
}

Lists

列表

Bullet list:
typescript
{
  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:
typescript
{
  type: 'orderedList',
  content: [
    // Same listItem structure as bulletList
  ]
}
无序列表:
typescript
{
  type: 'bulletList',
  content: [
    {
      type: 'listItem',
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '第一项'
            }
          ]
        }
      ]
    },
    {
      type: 'listItem',
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '第二项'
            }
          ]
        }
      ]
    }
  ]
}
有序列表:
typescript
{
  type: 'orderedList',
  content: [
    // 与无序列表的listItem结构相同
  ]
}

Helper Functions

辅助函数

typescript
// 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
.

typescript
// 创建简单文本段落
function createParagraph(text: string) {
  return {
    type: 'paragraph',
    content: [
      {
        type: 'text',
        text
      }
    ]
  };
}

// 创建ADF文档
function createADFDocument(...paragraphs: any[]) {
  return {
    type: 'doc',
    version: 1,
    content: paragraphs
  };
}

// 使用示例
const doc = createADFDocument(
  createParagraph('第一段'),
  createParagraph('第二段')
);
完整的ADF规范和模板,请查看
references/adf-format-guide.md
examples/adf-comment-templates.md

Issue Detection and Parsing

问题检测与解析

Jira URL Patterns

Jira URL模式

typescript
// 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" }]
typescript
// Jira问题URL模式
const JIRA_ISSUE_URL = /https?:\/\/([^\/]+)\.atlassian\.net\/browse\/([A-Z]+-\d+)/g;

// 自定义Jira域名
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]
  }));
}

// 示例
const text = "查看 https://mycompany.atlassian.net/browse/PROJ-123";
const issues = detectJiraIssues(text);
// => [{ url: "...", domain: "mycompany", key: "PROJ-123" }]

Jira Issue Key Pattern

Jira问题键模式

typescript
// 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"]
typescript
// 问题键模式(例如: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]);
}

// 示例
const text = "实现PROJ-123并修复PROJ-456";
const keys = extractJiraKeys(text);
// => ["PROJ-123", "PROJ-456"]

Auto-fetch Issue Details

自动获取问题详情

typescript
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;
}

typescript
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}`
  };
}

// 自动为文本补充问题详情
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管理

Sprint Planning Workflow

Sprint规划工作流

bash
undefined
bash
undefined

1. Create new sprint

1. 创建新Sprint

acli jira --action createSprint
--board "PROJ Board"
--name "Sprint 25"
--startDate "2024-01-15"
--endDate "2024-01-28"
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)

2. 将问题添加到Sprint(来自JQL查询)

acli jira --action getIssueList
--jql "project = PROJ AND labels = 'sprint-ready'"
--outputFormat 999 |
acli jira --action addIssuesToSprint
--sprint "Sprint 25"
--issue "@-"
acli jira --action getIssueList
--jql "project = PROJ AND labels = 'sprint-ready'"
--outputFormat 999 |
acli jira --action addIssuesToSprint
--sprint "Sprint 25"
--issue "@-"

3. Start sprint

3. 启动Sprint

acli jira --action startSprint
--sprint "Sprint 25"
undefined
acli jira --action startSprint
--sprint "Sprint 25"
undefined

Sprint Reporting

Sprint报告

typescript
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
  };
}

typescript
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命令

  • acli jira --action getIssueList --jql "query"
    - Query issues
  • acli jira --action createIssue --project PROJ --type Story --summary "..."
    - Create issue
  • acli jira --action transitionIssue --issue KEY --transition "Status"
    - Change status
  • acli jira --action addIssuesToSprint --sprint "Sprint" --issue "KEY"
    - Add to sprint
  • acli jira --action getSprintList --board "Board"
    - List sprints
  • 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"
    - 添加到Sprint
  • acli jira --action getSprintList --board "Board"
    - 列出Sprint

Key API Endpoints

关键API端点

  • GET /issue/{issueKey}
    - Get issue
  • POST /issue
    - Create issue
  • PUT /issue/{issueKey}
    - Update issue
  • POST /issue/{issueKey}/transitions
    - Transition issue
  • POST /issue/{issueKey}/comment
    - Add comment
  • GET /issue/{issueKey}
    - 获取问题
  • POST /issue
    - 创建问题
  • PUT /issue/{issueKey}
    - 更新问题
  • POST /issue/{issueKey}/transitions
    - 流转状态
  • POST /issue/{issueKey}/comment
    - 添加评论

JQL Quick Patterns

JQL快速模式

  • project = PROJ AND assignee = currentUser()
    - My issues
  • sprint in openSprints()
    - Current sprint
  • status = "To Do" ORDER BY priority DESC
    - Prioritized backlog
  • created >= -7d
    - Recent issues
  • project = PROJ AND assignee = currentUser()
    - 我的问题
  • sprint in openSprints()
    - 当前Sprint
  • status = "To Do" ORDER BY priority DESC
    - 按优先级排序的待办
  • created >= -7d
    - 近期问题

ADF Basics

ADF基础

  • Paragraph:
    {type: 'paragraph', content: [{type: 'text', text: '...'}]}
  • Bold:
    marks: [{type: 'strong'}]
  • Code:
    marks: [{type: 'code'}]
  • Link:
    marks: [{type: 'link', attrs: {href: '...'}}]
  • 段落:
    {type: 'paragraph', content: [{type: 'text', text: '...'}]}
  • 加粗:
    marks: [{type: 'strong'}]
  • 代码:
    marks: [{type: 'code'}]
  • 链接:
    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)

  • 始终使用API令牌而非密码进行认证
  • 缓存Jira项目元数据以减少API调用
  • 复杂查询使用JQL而非在代码中过滤
  • API调用前验证问题键格式(格式:PROJECT-123)
  • 所有富文本内容(描述、评论)使用ADF
  • 处理Jira API速率限制(10请求/秒)

Multi-Specialist ADF Comment Support

多专家ADF评论支持

When working with multi-specialist implementations, Jira comments need to aggregate work from multiple specialists into a single, well-formatted comment.
在多专家协作的实现场景中,Jira评论需要将多位专家的工作汇总为格式规范的单条评论。

Detection Logic

检测逻辑

Check for multi-specialist mode:
bash
undefined
检测是否为多专家模式:
bash
undefined

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**:
```bash
FEATURE_NAME="authentication" # 从问题或上下文提取
if [ -d ".agency/handoff/${FEATURE_NAME}" ]; then echo "检测到多专家模式" MODE="multi-specialist" else echo "单专家模式" MODE="single-specialist" fi

**收集专家信息**:
```bash

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
undefined
if [ -d ".agency/handoff/${FEATURE_NAME}" ]; then specialists=$(ls -d .agency/handoff/${FEATURE_NAME}/*/ | xargs -n1 basename)
for specialist in $specialists; do echo "发现专家: $specialist"
# 读取专家的摘要
if [ -f ".agency/handoff/${FEATURE_NAME}/${specialist}/summary.md" ]; then
  cat ".agency/handoff/${FEATURE_NAME}/${specialist}/summary.md"
fi

# 读取专家的验证结果
if [ -f ".agency/handoff/${FEATURE_NAME}/${specialist}/verification.md" ]; then
  cat ".agency/handoff/${FEATURE_NAME}/${specialist}/verification.md"
fi
done fi
undefined

Multi-Specialist ADF Comment Template

多专家ADF评论模板

Complete example with multiple specialists:
typescript
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:
typescript
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 });
包含多位专家的完整示例:
typescript
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: [
      // 顶部的整体状态面板
      {
        type: 'panel',
        attrs: {
          panelType: panelType[overallStatus]
        },
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: `${statusEmoji[overallStatus]} 多专家实现完成`,
                marks: [{ type: 'strong' }]
              }
            ]
          },
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: `功能: ${featureName} | 专家数量: ${specialists.length}`
              }
            ]
          }
        ]
      },

      // 专家摘要
      {
        type: 'heading',
        attrs: { level: 3 },
        content: [
          {
            type: 'text',
            text: '专家贡献'
          }
        ]
      },

      // 带状态表情的专家列表
      {
        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}`
                }
              ]
            }
          ]
        }))
      },

      // 按专家拆分的详细工作(类折叠面板)
      {
        type: 'heading',
        attrs: { level: 3 },
        content: [
          {
            type: 'text',
            text: '详细工作分解'
          }
        ]
      },

      ...specialists.flatMap(specialist => [
        // 专家标题
        {
          type: 'heading',
          attrs: { level: 4 },
          content: [
            {
              type: 'text',
              text: `${specialist.displayName} ${statusEmoji[specialist.status]}`
            }
          ]
        },

        // 摘要
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '摘要: ',
              marks: [{ type: 'strong' }]
            },
            {
              type: 'text',
              text: specialist.summary
            }
          ]
        },

        // 修改的文件
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '修改的文件: ',
              marks: [{ type: 'strong' }]
            },
            {
              type: 'text',
              text: `${specialist.filesChanged.length} 个文件`
            }
          ]
        },
        {
          type: 'bulletList',
          content: specialist.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: '测试结果: ',
              marks: [{ type: 'strong' }]
            },
            {
              type: 'text',
              text: specialist.testResults
            }
          ]
        }
      ]),

      // 集成点
      {
        type: 'heading',
        attrs: { level: 3 },
        content: [
          {
            type: 'text',
            text: '集成点'
          }
        ]
      },
      {
        type: 'bulletList',
        content: integrationPoints.map(point => ({
          type: 'listItem',
          content: [
            {
              type: 'paragraph',
              content: [
                {
                  type: 'text',
                  text: point
                }
              ]
            }
          ]
        }))
      }
    ]
  };
}
使用示例:
typescript
const specialists: SpecialistWork[] = [
  {
    name: 'backend-architect',
    displayName: '后端架构师',
    summary: '实现了带JWT和刷新令牌的认证API',
    filesChanged: [
      'src/api/auth/login.ts',
      'src/api/auth/refresh.ts',
      'src/middleware/authenticate.ts',
      'src/models/user.ts'
    ],
    testResults: '所有测试通过 (24/24)',
    status: 'success'
  },
  {
    name: 'frontend-developer',
    displayName: '前端开发工程师',
    summary: '创建了登录/注册表单并与认证API集成',
    filesChanged: [
      'src/components/LoginForm.tsx',
      'src/components/SignupForm.tsx',
      'src/hooks/useAuth.ts',
      'src/pages/profile.tsx'
    ],
    testResults: '所有测试通过 (18/18)',
    status: 'success'
  }
];

const comment = createMultiSpecialistComment(
  '认证系统',
  specialists,
  'success',
  [
    '后端暴露/api/auth/login和/api/auth/refresh端点',
    '前端使用useAuth钩子管理认证状态',
    'JWT令牌存储在httpOnly Cookie中',
    '未认证时受保护路由重定向到登录页'
  ]
);

// 发布到Jira
await jiraClient.post(`/issue/PROJ-123/comment`, { body: comment });

Single-Specialist ADF Comment Template

单专家ADF评论模板

Backward compatibility - Keep existing single-specialist format:
typescript
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
          }
        ]
      }
    ]
  };
}
向后兼容 - 保留现有单专家格式:
typescript
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]} 实现完成`,
                marks: [{ type: 'strong' }]
              }
            ]
          }
        ]
      },
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '摘要: ',
            marks: [{ type: 'strong' }]
          },
          {
            type: 'text',
            text: summary
          }
        ]
      },
      {
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: '修改的文件: ',
            marks: [{ type: 'strong' }]
          },
          {
            type: 'text',
            text: `${filesChanged.length} 个文件`
          }
        ]
      },
      {
        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: '测试结果: ',
            marks: [{ type: 'strong' }]
          },
          {
            type: 'text',
            text: testResults
          }
        ]
      }
    ]
  };
}

Auto-Detection Wrapper

自动检测包装器

Automatically choose the right format:
typescript
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 });
  }
}
自动选择合适的格式:
typescript
async function postImplementationComment(
  issueKey: string,
  featureName: string
): Promise<void> {
  const handoffDir = `.agency/handoff/${featureName}`;

  // 检查是否为多专家模式
  if (fs.existsSync(handoffDir)) {
    // 多专家模式
    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')
          : '';

        // 解析摘要和验证结果以提取数据
        const specialist = parseSpecialistData(specialistName, summary, verification);
        specialists.push(specialist);
      }
    }

    // 确定整体状态
    const overallStatus = specialists.every(s => s.status === 'success')
      ? 'success'
      : specialists.some(s => s.status === 'error')
      ? 'error'
      : 'warning';

    // 从摘要中提取集成点
    const integrationPoints = extractIntegrationPoints(specialists);

    const comment = createMultiSpecialistComment(
      featureName,
      specialists,
      overallStatus,
      integrationPoints
    );

    await jiraClient.post(`/issue/${issueKey}/comment`, { body: comment });
  } else {
    // 单专家模式(向后兼容)
    const summary = '实现完成';
    const filesChanged = await getChangedFiles();
    const testResults = '所有测试通过';
    const status = 'success';

    const comment = createSingleSpecialistComment(
      summary,
      filesChanged,
      testResults,
      status
    );

    await jiraClient.post(`/issue/${issueKey}/comment`, { body: comment });
  }
}

Helper Functions

辅助函数

typescript
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 [];
  }
}
typescript
function parseSpecialistData(
  name: string,
  summary: string,
  verification: string
): SpecialistWork {
  // 提取显示名称
  const displayNames: Record<string, string> = {
    'backend-architect': '后端架构师',
    'frontend-developer': '前端开发工程师',
    'database-specialist': '数据库专家',
    'devops-engineer': 'DevOps工程师'
  };

  // 提取摘要(第一段或标题)
  const summaryMatch = summary.match(/^##?\s+(.+)$/m) ||
                       summary.match(/^(.+)$/m);
  const summaryText = summaryMatch ? summaryMatch[1] : '工作完成';

  // 从摘要中提取文件(查找代码块或列表)
  const filesMatch = summary.match(/```[^`]*```/s) ||
                     summary.match(/^[-*]\s+`([^`]+)`/gm);
  const filesChanged = filesMatch
    ? Array.from(summary.matchAll(/`([^`]+\.[a-z]+)`/g)).map(m => m[1])
    : [];

  // 提取测试结果
  const testMatch = verification.match(/Tests?:\s*(.+)/i) ||
                   verification.match(/(\d+\/\d+\s+passing)/i);
  const testResults = testMatch ? testMatch[1] : '测试完成';

  // 从验证结果中确定状态
  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[] = [];

  // 从后端专家中查找API端点
  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 => `后端暴露 ${api} 端点`));
    }
  }

  // 从前端专家中查找组件
  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(`前端组件: ${componentMatches.join(', ')}`);
    }
  }

  return points.length > 0 ? points : ['请查看各专家部分了解详情'];
}

async function getChangedFiles(): Promise<string[]> {
  // 使用execFile从git获取修改的文件以保证安全
  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('获取修改的文件失败:', error);
    return [];
  }
}

Best Practices

最佳实践

Multi-specialist comments:
  1. Always include overall status panel at top
  2. List all specialists with emoji status indicators
  3. Provide detailed breakdown per specialist
  4. Highlight integration points between specialists
  5. Keep file lists reasonable (max 10 per specialist)
  6. Include test results for each specialist
  7. Use consistent formatting across all specialists
ADF structure:
  1. Use panels for status (success/warning/error)
  2. Use headings (level 3-4) for major sections
  3. Use bullet lists for file listings
  4. Use code marks for file paths and code references
  5. Keep paragraphs focused and concise
  6. Always validate ADF structure before posting
Detection logic:
  1. Always check for
    .agency/handoff/{feature}
    directory first
  2. Fall back to single-specialist format if not found
  3. Handle missing files gracefully (summary.md, verification.md)
  4. Parse specialist data defensively with defaults
  5. Validate all parsed data before creating ADF

多专家评论:
  1. 始终在顶部包含整体状态面板
  2. 列出所有专家并附带状态表情
  3. 提供每位专家的详细工作分解
  4. 突出专家之间的集成点
  5. 文件列表保持合理长度(每位专家最多10个)
  6. 包含每位专家的测试结果
  7. 所有专家使用一致的格式
ADF结构:
  1. 使用面板显示状态(成功/警告/错误)
  2. 使用标题(3-4级)划分主要部分
  3. 使用无序列表展示文件列表
  4. 使用代码标记显示文件路径和代码引用
  5. 段落内容聚焦且简洁
  6. 发布前始终验证ADF结构
检测逻辑:
  1. 始终优先检查
    .agency/handoff/{feature}
    目录
  2. 未找到则回退到单专家格式
  3. 优雅处理缺失文件(summary.md、verification.md)
  4. 带默认值的防御式专家数据解析
  5. 创建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
  • jira-adf-generator: 为Jira评论生成格式规范的ADF
  • github-integration: 可对比的其他平台集成
  • agency-workflow-patterns: 适用于任意平台的通用工作流自动化
  • github-workflow-best-practices: Sprint概念和规划模式