mcpField
J
jira-safe

jira-safe

github.com/01000001-01001110開発10181 回インストール46 週間前に更新
リポジトリ

jira-safe について

Jira CloudでScaled Agile Frameworkに沿って作業項目を管理するスキルです。SAFe形式の計画と階層を扱い、Next-GenとClassicのプロジェクト差異にも対応します。

主な機能

  1. 01Epic、Feature、Story、Subtaskの作成
  2. 02SAFe形式のユーザーストーリー作成
  3. 03受け入れ条件の定義
  4. 04作業項目の階層的な関連付け
  5. 05Jiraプロジェクト種別の判定

活用シーン

  1. 01SAFeの階層に沿った作業計画
  2. 02FeatureをStoryに分割
  3. 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.mjs
add-subtasks.py
create-mvp.mjs
create-mvp.py
create-two-level.mjs
create-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 Linkcustomfield_10014parent: { key: 'EPIC-KEY' }
Epic Namecustomfield_10011利用不可
Subtask のタイプ'Sub-task''Subtask'
プロジェクトスタイルclassicnext-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 → Epicparent: { key: 'EPIC-KEY' }
親(Next-Gen)Subtask → Storyparent: { key: 'STORY-KEY' }
ブロックする/ブロックされる依存関係リンクの種類
関連する関連項目リンクの種類

Classic プロジェクトのみ:

リンクの種類使用例フィールド
Epic LinkStory → Epiccustomfield_10014
Epic NameEpic の短縮名customfield_10011

プロジェクトの種類別カスタムフィールド

Next-Gen(チーム管理型) - SCRUM プロジェクト
目的方法
Story を Epic にリンクするparent: { key: 'EPIC-KEY' }
Subtask を Story にリンクするparent: { key: 'STORY-KEY' }
Subtask の課題タイプissuetype: { name: 'Subtask' }
Classic(会社管理型)
フィールドID(一般的な値)目的
Epic Linkcustomfield_10014Story を Epic にリンクします
Epic Namecustomfield_10011Epic の短縮名
Story Pointscustomfield_10016見積もり
Sprintcustomfield_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();
}

参考資料

コメント