jira-safe について
Jira CloudでScaled Agile Frameworkに沿って作業項目を管理するスキルです。SAFe形式の計画と階層を扱い、Next-GenとClassicのプロジェクト差異にも対応します。
主な機能
- 01Epic、Feature、Story、Subtaskの作成
- 02SAFe形式のユーザーストーリー作成
- 03受け入れ条件の定義
- 04作業項目の階層的な関連付け
- 05Jiraプロジェクト種別の判定
活用シーン
- 01SAFeの階層に沿った作業計画
- 02FeatureをStoryに分割
- 03StoryとSubtaskの作成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ 01000001-01001110/ agent-jira-skills --skill jira-safeインストール後、Claude Code で/jira-safeを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
scripts/add-subtasks.mjsadd-subtasks.pycreate-mvp.mjscreate-mvp.pycreate-two-level.mjscreate-two-level.py詳細
Jira SAFe(Scaled Agile Framework)スキル
Jira Cloud で Epic、Feature、Story、Task を管理するための SAFe 手法を実装します。
使用する場面
- ビジネス成果と受け入れ条件を含む Epic を作成する
- SAFe 形式(「As a... I want... So that...」)でユーザーストーリーを作成する
- 受け入れ条件を設定して Feature を Story に分割する
- Story の下に Subtask を作成する
- 作業項目を適切な階層(Epic → Feature → Story → Subtask)でリンクする
重要:Next-Gen プロジェクトと Classic プロジェクトの違い
SCRUM プロジェクトは Next-Gen(チーム管理型)です。 主な違いは次のとおりです。
| 項目 | Classic(会社管理型) | Next-Gen(チーム管理型) |
|---|---|---|
| Epic Link | customfield_10014 | parent: { key: 'EPIC-KEY' } |
| Epic Name | customfield_10011 | 利用不可 |
| Subtask のタイプ | 'Sub-task' | 'Subtask' |
| プロジェクトスタイル | classic | next-gen, simplified: true |
必ず最初にプロジェクトの種類を検出してください。
const projectInfo = await fetch(`${JIRA_URL}/rest/api/3/project/${PROJECT_KEY}`, { headers });
const project = await projectInfo.json();
const isNextGen = project.style === 'next-gen' || project.simplified === true;
Jira における SAFe の階層
Portfolio Level:
└── Epic (Strategic Initiative)
└── Feature (Benefit Hypothesis)
└── Story (User Value)
└── Subtask (Technical Work)
SAFe テンプレート
Epic テンプレート(Next-Gen)
// NOTE: Next-Gen projects do NOT use customfield_10011 (Epic Name)
const epic = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Epic' },
summary: '[Epic ID]: [Epic Name] - [Business Outcome]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Business Outcome' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Describe the measurable business value...' }]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Success Metrics' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Metric 1: [measurable target]' }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Scope' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'What is in scope and out of scope...' }]
}
]
},
labels: ['epic-label'] // Use labels instead of Epic Name for categorization
}
};
Story テンプレート(SAFe 形式、Next-Gen)
// NOTE: Next-Gen uses 'parent' field, NOT customfield_10014
const story = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Story' },
summary: '[US-ID]: As a [persona], I want [goal], so that [benefit]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'User Story' }]
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'As a ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[persona]' },
{ type: 'text', text: ', I want ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[goal]' },
{ type: 'text', text: ', so that ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[benefit]' }
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Acceptance Criteria' }]
},
{
type: 'heading',
attrs: { level: 3 },
content: [{ type: 'text', text: 'Scenario 1: [Name]' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'GIVEN [precondition]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'WHEN [action]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'THEN [expected result]', marks: [{ type: 'strong' }] }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Definition of Done' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Code reviewed and approved' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Unit tests written and passing' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Integration tests passing' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Documentation updated' }] }]
}
]
}
]
},
// Next-Gen: Link to parent Epic using 'parent' field
parent: { key: 'EPIC_KEY' },
labels: ['category-label', 'epic-id']
}
};
Subtask テンプレート(Next-Gen)
// NOTE: Next-Gen uses 'Subtask' (no hyphen), NOT 'Sub-task'
const subtask = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', Classic: 'Sub-task'
summary: '[Technical task description]',
// Parent Story (required for subtasks)
parent: { key: 'STORY_KEY' }
// Note: Description is optional for subtasks
}
};
API の実装(Next-Gen プロジェクト)
Story を含む Epic を作成する(Next-Gen)
async function createEpicWithStories(epicFields, storyDefinitions) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. Create Epic
const epicResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: epicFields })
});
if (!epicResponse.ok) {
const error = await epicResponse.text();
throw new Error(`Epic creation failed: ${error}`);
}
const createdEpic = await epicResponse.json();
console.log(`Created Epic: ${createdEpic.key}`);
// 2. Create Stories linked to Epic using 'parent' field (Next-Gen)
const createdStories = [];
for (const storyDef of storyDefinitions) {
const storyFields = {
...storyDef,
parent: { key: createdEpic.key } // Next-Gen: use 'parent', NOT customfield_10014
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: storyFields })
});
if (!storyResponse.ok) {
const error = await storyResponse.text();
console.error(`Story creation failed: ${error}`);
continue;
}
const createdStory = await storyResponse.json();
createdStories.push(createdStory);
console.log(` Created Story: ${createdStory.key}`);
// Rate limiting
await new Promise(r => setTimeout(r, 100));
}
return { epic: createdEpic, stories: createdStories };
}
Subtask を含む Story を作成する(Next-Gen)
async function createStoryWithSubtasks(storyFields, epicKey, subtaskSummaries) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. Create Story under Epic
const storyRequest = {
fields: {
...storyFields,
parent: { key: epicKey } // Link to Epic
}
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify(storyRequest)
});
if (!storyResponse.ok) {
throw new Error(`Story creation failed: ${await storyResponse.text()}`);
}
const createdStory = await storyResponse.json();
// 2. Create Subtasks under Story
const createdSubtasks = [];
for (const summary of subtaskSummaries) {
const subtaskResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({
fields: {
project: { key: storyFields.project.key },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', NOT 'Sub-task'
summary: summary,
parent: { key: createdStory.key }
}
})
});
if (subtaskResponse.ok) {
createdSubtasks.push(await subtaskResponse.json());
}
await new Promise(r => setTimeout(r, 50)); // Rate limiting
}
return { story: createdStory, subtasks: createdSubtasks };
}
Epic Link フィールド ID を取得する
Epic Link フィールドは Jira インスタンスによって異なります。次の方法で確認してください。
async function findEpicLinkField() {
const response = await fetch(`${JIRA_URL}/rest/api/3/field`, { headers });
const fields = await response.json();
const epicLinkField = fields.find(f =>
f.name === 'Epic Link' ||
f.name.toLowerCase().includes('epic link')
);
return epicLinkField?.id; // Usually customfield_10014
}
課題を一括削除する
async function bulkDeleteIssues(projectKey, maxResults = 100) {
// Search for all issues
const jql = encodeURIComponent(`project = ${projectKey} ORDER BY key ASC`);
const searchResponse = await fetch(
`${JIRA_URL}/rest/api/3/search/jql?jql=${jql}&maxResults=${maxResults}&fields=key`,
{ headers }
);
const { issues } = await searchResponse.json();
// Delete each issue
for (const issue of issues) {
await fetch(`${JIRA_URL}/rest/api/3/issue/${issue.key}?deleteSubtasks=true`, {
method: 'DELETE',
headers
});
console.log(`Deleted: ${issue.key}`);
await new Promise(r => setTimeout(r, 100)); // Rate limit
}
return issues.length;
}
SAFe のベストプラクティス
Epic の命名
- 形式:
[Domain] - [Business Outcome] - 例:
Marketing Copilot - Enable 24/7 Brand-Aware Content Generation
Story の命名(INVEST 基準)
- Independent:個別に開発できます
- Negotiable:詳細について議論できます
- Valuable:ユーザーに価値を提供します
- Estimable:規模を見積もることができます
- Small:スプリント内に収まります
- Testable:明確な受け入れ条件があります
Story の形式
As a [specific persona],
I want [concrete action/capability],
So that [measurable benefit].
受け入れ条件(Given-When-Then)
Scenario: [Descriptive name]
GIVEN [initial context/precondition]
WHEN [action/event occurs]
THEN [expected outcome]
AND [additional outcome if needed]
課題リンクの種類(Next-Gen)
| リンクの種類 | 使用例 | フィールド |
|---|---|---|
| 親(Next-Gen) | Story → Epic | parent: { key: 'EPIC-KEY' } |
| 親(Next-Gen) | Subtask → Story | parent: { key: 'STORY-KEY' } |
| ブロックする/ブロックされる | 依存関係 | リンクの種類 |
| 関連する | 関連項目 | リンクの種類 |
Classic プロジェクトのみ:
| リンクの種類 | 使用例 | フィールド |
|---|---|---|
| Epic Link | Story → Epic | customfield_10014 |
| Epic Name | Epic の短縮名 | customfield_10011 |
プロジェクトの種類別カスタムフィールド
Next-Gen(チーム管理型) - SCRUM プロジェクト
| 目的 | 方法 |
|---|---|
| Story を Epic にリンクする | parent: { key: 'EPIC-KEY' } |
| Subtask を Story にリンクする | parent: { key: 'STORY-KEY' } |
| Subtask の課題タイプ | issuetype: { name: 'Subtask' } |
Classic(会社管理型)
| フィールド | ID(一般的な値) | 目的 |
|---|---|---|
| Epic Link | customfield_10014 | Story を Epic にリンクします |
| Epic Name | customfield_10011 | Epic の短縮名 |
| Story Points | customfield_10016 | 見積もり |
| Sprint | customfield_10007 | スプリントの割り当て |
エラー処理
async function safeJiraRequest(url, options = {}) {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Jira API ${response.status}: ${error.substring(0, 200)}`);
}
if (response.status === 204) return null;
return response.json();
}
参考資料
コメント
Markdown 対応