Loading...
Loading...
Compare original and translation side by side
acliacliundefinedundefined
**Configuration file** (`~/.acli/acli.properties`):
```properties
server=https://your-domain.atlassian.net
user=user@example.com
password=your-api-token
**配置文件**(`~/.acli/acli.properties`):
```properties
server=https://your-domain.atlassian.net
user=user@example.com
password=your-api-tokenundefinedundefined
**Get issue details**:
```bash
**获取问题详情**:
```bash
**Create issue**:
```bash
**创建问题**:
```bash
**Update issue**:
```bash
**更新问题**:
```bash
**Transition issue**:
```bash
**流转问题状态**:
```bash
**Assign issue**:
```bash
**分配问题**:
```bashundefinedundefinedundefinedundefined
**Add issues to sprint**:
```bash
**添加问题到Sprint**:
```bash
**Start sprint**:
```bash
**启动Sprint**:
```bash
**Close sprint**:
```bash
**关闭Sprint**:
```bashundefinedundefinedundefinedundefined
**Get board configuration**:
```bash
**获取看板配置**:
```bashundefinedundefinedundefinedundefined
**Bulk update**:
```bash
**批量更新**:
```bash
For complete acli command reference, see `references/acli-reference.md`.
---
完整的acli命令参考,请查看`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'
}
});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'
}
});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
}
});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: '实现认证功能',
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}`);await jiraClient.put(`/issue/PROJ-123`, {
fields: {
summary: '更新后的摘要',
labels: ['bug', 'urgent']
}
});// 获取可用的状态流转
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
}
});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.mdawait jiraClient.post(`/issue/PROJ-123/comment`, {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: '该问题已审核通过'
}
]
}
]
}
});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.mdundefinedundefinedundefinedundefinedundefinedundefined
**By assignee**:
```jql
**按经办人**:
```jql
**By date**:
```jql
**按日期**:
```jql
**By sprint**:
```jql
**按Sprint**:
```jql
**By label**:
```jql
**按标签**:
```jqlundefinedundefinedundefinedundefined
**Using functions**:
```jql
**使用函数**:
```jql
**Ordering results**:
```jql
**结果排序**:
```jql
For 30+ JQL query examples, see `examples/jql-query-examples.md`.
---
30+个JQL查询示例,请查看`examples/jql-query-examples.md`。
---// Simple text paragraph
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Hello, world!'
}
]
}
]
};// 简单文本段落
const adf = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: '你好,世界!'
}
]
}
]
};{
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: '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' }]
}
]
}
]
}{
type: 'text',
text: 'Click here',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com'
}
}
]
}{
type: 'text',
text: '点击这里',
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: '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
]
}{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: '第一项'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: '第二项'
}
]
}
]
}
]
}{
type: 'orderedList',
content: [
// 与无序列表的listItem结构相同
]
}// 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// 创建简单文本段落
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('第二段')
);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" }]// 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" }]// 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"]// 问题键模式(例如: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"]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;
}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;
}undefinedundefinedundefinedundefinedinterface 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
};
}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"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}/commentGET /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 >= -7dproject = 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: '...'}}]{type: 'paragraph', content: [{type: 'text', text: '...'}]}marks: [{type: 'strong'}]marks: [{type: 'code'}]marks: [{type: 'link', attrs: {href: '...'}}]undefinedundefined
**Gather specialist information**:
```bash
**收集专家信息**:
```bash# 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"
fiundefined# 读取专家的摘要
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"
fiundefinedinterface 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 });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
}
]
}
]
}))
}
]
};
}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 });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
}
]
}
]
};
}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
}
]
}
]
};
}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 });
}
}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 });
}
}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 [];
}
}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 [];
}
}.agency/handoff/{feature}.agency/handoff/{feature}