mcpField
C
create-issue-tree

create-issue-tree

github.com/fandhe-ai开发21.7k 次安装4 天前更新
代码仓库

关于 create-issue-tree

create-issue-tree 是一项根据需求或任务列表创建 GitHub Issue 树的技能,适用于需要分阶段规划和跟踪开发任务的项目。它可建立根跟踪 issue、Phase 父 issue、子 issue 和 sub-issue,并支持部分 Phase 起票及复用已有根 issue。

主要功能

  1. 01按需求拆分并创建 Issue 树
  2. 02支持根、Phase、子任务层级
  3. 03按 Phase 部分起票并复用根 issue
  4. 04设置粒度上限与 GitHub Milestone
  5. 05从实际子 Issue 生成根 issue 汇总

适用场景

  1. 01将功能需求拆成可跟踪的 GitHub 任务
  2. 02分阶段创建大型实施计划
  3. 03向已有 Issue 树追加后续 Phase

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/fandhe-ai/agent-cli-skills --skill create-issue-tree

安装后在 Claude Code 中输入/create-issue-tree确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

scripts/
create-root-body.sh
merge-root-body.sh
tree-lib.sh
tests/
step6-root-body.test.mjs

详细介绍

create-issue-tree

要件・タスク一覧から Phase 分割された GitHub Issue ツリーを新規作成する。 ルート(トラッキング issue)→ Phase 親 issue → issue → sub-issue の 4 階層を構築し、implement-issue-tree が post-order DFS で消化できる構造を維持する。

ルート issue 本文の運用ルール「実行順は sub-issues リスト順が正」は既定では post-order の優先度に留まり、並列実行時に後続 Phase が前 Phase を追い越す余地がある。Phase 完了を厳密に順序保証したい場合は implement-issue-tree 実行時に phaseGate: true を指定する(implement-issue-tree の SKILL.md 参照)。

使い方

引数としてタスク要件テキストまたはファイルパスを渡す。
--phase オプションで特定 Phase のみ起票することもできる(大規模ツリーを段階的に起票する場合)。
2 回目以降の部分起票では --root で既存ルート issue 番号を渡し、同じツリーに継ぎ足す (指定しないと新しいルート issue が重複作成される)。
--milestone オプションで起票する issue 全件に割り当てる GitHub Milestone を指定できる (--root 指定時は省略可。既存ルートの milestone を自動継承する)。 --granularity オプションで 1 issue に収める実装時間の上限を指定できる。 2h・4h・1h のように正整数+時間単位(h)で指定する。優先順位は --granularity 明示 > --root 指定時の既存ルート issue 本文マーカー > 既定 2h の順 (Step 1 で確定)。決定した値はルート issue 本文へ `` として 永続化され、同じルートに対する update-issue-tree 実行時に継承される。

create-issue-tree "ユーザー認証機能を実装する"
create-issue-tree requirements.md
create-issue-tree requirements.md --phase 2 --root 123
create-issue-tree requirements.md --milestone "v2.0"
create-issue-tree requirements.md --granularity 4h

前提条件

  • gh CLI がインストールされ、認証済みであること(gh auth status で確認)
  • 対象リポジトリへの Issue 書き込み権限があること

フロー

Step 1: 要件を分析してタスクを分解する

入力テキストまたはファイルから要件を読み込み、以下の観点でタスクを分解する。

  • 粒度基準: 1 issue は実装 ${GRANULARITY}(既定 2h・--granularity で変更可)程度に収める。 超えると判断した場合は sub-issue に再分解する

--granularity の値は代入前に Claude 側でも同じ正規表現(^[1-9][0-9]*h$)で検証し、 不一致ならこのフェンスを実行せずユーザーに再指定を求める(未信頼値を生成済みシェルへ 埋め込まない)。優先順位は使い方節のとおり、--granularity > ルートのマーカー > 既定 2h の順。

# --granularity で渡された時間を実際の値で代入する(実行時に Claude が置き換える)
# 例: --granularity 4h が指定された場合 → GRANULARITY_ARG="4h" / 未指定の場合は空文字
GRANULARITY_ARG="<--granularity で渡された時間(未指定なら空文字)>"
# --root で渡された Issue 番号(未指定なら空文字)。Step 2.5 で ROOT_NUMBER として
# OPEN 検証込みで正式に確定する前に、粒度継承の参照のためここでも読む
ROOT_ARG="<--root で渡された Issue 番号(未指定なら空文字)>"

if [[ -n "${GRANULARITY_ARG}" ]]; then
  GRANULARITY="${GRANULARITY_ARG}"
  GRANULARITY_SOURCE="--granularity 引数"
elif [[ -n "${ROOT_ARG}" ]]; then
  # ルート本文の取得失敗とマーカー不在は分離する。取得失敗は「粒度を既定へ倒さず中止」する
  # (gh issue view 自体の失敗を || true で握り潰すと、権限エラー・issue 不在等を
  # 「マーカーなし」と誤読して既定 2h へサイレントに倒れてしまうため)
  ROOT_BODY=$(gh issue view "${ROOT_ARG}" --json body --jq '.body') \
    || { echo "エラー: ルート issue #${ROOT_ARG} の本文を取得できません。粒度を既定へ倒さず中止します。"; exit 1; }
  # 既存ルート本文の  マーカーから継承する。マーカーが無ければ既定 2h
  # (|| true は grep のマーカー不在にのみ掛かる。gh issue view 自体の失敗は上で既に処理済み)
  ROOT_GRANULARITY=$(printf '%s\n' "${ROOT_BODY}" \
    | grep -oE '' | head -1 | grep -oE '[1-9][0-9]*h' || true)
  if [[ -n "${ROOT_GRANULARITY}" ]]; then
    GRANULARITY="${ROOT_GRANULARITY}"
    GRANULARITY_SOURCE="ルート issue #${ROOT_ARG} のマーカー"
  else
    GRANULARITY="2h"
    GRANULARITY_SOURCE="既定値"
  fi
else
  GRANULARITY="2h"
  GRANULARITY_SOURCE="既定値"
fi

# 許可する構文は正整数 + h のみ(例: 1h / 2h / 4h)。引用符・空白・コマンド置換・0h・単位なしは
# ここで拒否する(ROOT_GRANULARITY はマーカー抽出時点で同じ正規表現を通しているが、
# GRANULARITY_ARG はユーザー入力の生値のため、代入元によらずこの検証を必ず通す)
if ! printf '%s' "${GRANULARITY}" | grep -qE '^[1-9][0-9]*h$'; then
  echo "エラー: --granularity の値 '${GRANULARITY}' は不正です(許可: 正整数+h、例 2h)。中止します。"
  exit 1
fi

# 確定値と由来を必ず出力する(既定 2h と読み替えないことをここで可視化する)
echo "粒度基準: ${GRANULARITY}(由来: ${GRANULARITY_SOURCE})"

以降の Step は Step 1 で出力された GRANULARITY の値を使う(既定 2h と読み替えない)。

  • 各タスクの依存関係・実行順を把握する
  • タスク数を集計し、Phase 分割の要否を判断する(目安: 10 件超で Phase 分割を検討)

分解結果をユーザーに提示し、Phase 構成・issue 数について確認を取る。
--phase オプションが指定されている場合は該当 Phase のタスクのみ対象とする。

Step 2: Phase 構成を設計する

タスク数・依存関係をもとに Phase を設計する。

  • Phase は独立して開発可能な単位で分割する(例: Phase 1 基盤・Phase 2 機能・Phase 3 改善)
  • 各 Phase に親 issue タイトル(Conventional Commits 形式推奨)を割り当てる
  • 全体をまとめるルート(トラッキング)issue のタイトルを決定する
    • 例: chore(global): 全 open issue の Phase 別トラッキング (YYYY-MM-DD)

タスクが少数(Phase 分割不要)の場合はルート issue + 子 issue の 2 階層構成にする。

Step 2.5: milestone を決定する

このツリーに割り当てる GitHub Milestone を決定する。ルート・Phase 親・子・sub-issue の 全件に同一 milestone を適用する(ツリー単位で 1 milestone)。

--root が指定されている場合は、ここで先に ROOT_NUMBER を設定し、milestone の 継承・書き込みを行う前に OPEN 状態を検証する(closed なルートの milestone を 書き換えてから中断する事故を防ぐ。Step 3 側では再代入・再検証しない)。

# --root で渡された Issue 番号を実際の値で代入する(実行時に Claude が置き換える)
# 例: --root 123 が指定された場合 → ROOT_NUMBER="123" / --root 未指定の場合 → ROOT_NUMBER=""
ROOT_NUMBER="<--root で渡された Issue 番号(未指定なら空文字)>"

# --root 指定時のみ: OPEN でなければ milestone 操作より前に中止する
# (新規ツリー作成で ROOT_NUMBER が空の場合はこの検証をスキップする)
if [[ -n "${ROOT_NUMBER}" ]]; then
  ROOT_STATE=$(gh issue view "${ROOT_NUMBER}" --json state --jq '.state')
  if [[ "${ROOT_STATE}" != "OPEN" ]]; then
    echo "エラー: ルート issue #${ROOT_NUMBER} は OPEN ではありません (state: ${ROOT_STATE})。中止します。"
    exit 1
  fi
fi

milestone 非運用リポジトリのガード: --milestone が明示されていない場合、リポジトリに milestone が 1 件も存在しなければ(closed 含む)milestone 非運用リポジトリとみなし、 以降の確認をすべてスキップして MILESTONE は空のまま Step 3 へ進む (milestone を使わないリポジトリの起票フローに確認を増やさない)。

MILESTONE_COUNT=$(gh api "repos/{owner}/{repo}/milestones?state=all" --jq 'length')
# MILESTONE_COUNT が 0 かつ --milestone 未指定なら、このステップの残りをスキップする

優先順位は --milestone > --root からの継承 > ユーザーへの確認 の順。

  • --milestone が指定されている場合: その値をそのまま MILESTONE として使用する (--root も同時指定されている場合、ルート側の milestone より --milestone を優先する。 ルート側と異なる値の場合は、ルートの milestone も合わせて更新してよいかユーザーに確認する。 更新しないと回答された場合はツリー内で milestone が混在する点を伝えたうえで続行する)

  • --milestone 未指定かつ --root 指定時: 既存ルートの milestone を自動継承する (milestone が取得できた場合のみユーザーへの確認は不要)

    MILESTONE=$(gh issue view "${ROOT_NUMBER}" --json milestone --jq '.milestone.title // empty')
    

    MILESTONE が空(既存ルートに milestone が未設定)の場合は自動継承とみなさず、 「どちらも未指定の場合」と同じユーザー確認フローへ進む(リポジトリに milestone が 存在するのにサイレントに milestone なしで進行しない。milestone が 1 件もない場合は 冒頭の非運用ガードが先に働くため、この確認には到達しない)。

  • どちらも未指定の場合、または --root 指定時に継承すべき milestone が空だった場合: milestone を割り当てるかユーザーに確認する。 割り当てる場合はオープン中の milestone 一覧を提示して選ばせるか、新規 milestone 名の 入力を受け付けて MILESTONE に設定する。割り当てないと回答されたら MILESTONE は 空のまま Step 3 以降へ進む(issue は milestone なしで作成される)。

    gh api "repos/{owner}/{repo}/milestones" --jq '.[] | select(.state=="open") | .title'
    

    ユーザーが一覧にない新規 milestone 名を入力した場合、gh issue create --milestone は 既存の milestone 名しか受け付けないため、使用前に milestone 自体を作成する。 同名の closed milestone が既に存在すると作成が 422(already_exists)で失敗するため、 その場合は reopen するか別名にするかをユーザーに確認する。

    gh api --method POST "repos/{owner}/{repo}/milestones" -f "title=${MILESTONE}"
    

    --root 指定時に継承すべき milestone が空でこのフローに合流した場合、決定した MILESTONE を既存ルート issue にも反映する(子だけ milestone が付き、ルートが 未設定のまま残る不整合を防ぐ)。

    if [[ -n "${ROOT_NUMBER}" && -n "${MILESTONE}" ]]; then
      gh issue edit "${ROOT_NUMBER}" --milestone "${MILESTONE}"
    fi
    
Step 3: ルート(トラッキング)issue を作成する

ルート issue はツリー全体の進捗を管理するトラッキング issue として作成する。ルート issue 自体には phase ラベルは付与しない(Phase 親以下の issue にのみ付与する)。

--root 指定時は新規作成をスキップする。 --phase での 2 回目以降の部分起票で ルート issue を重複作成しないため、Step 2.5 で設定・OPEN 検証済みの ROOT_NUMBER を そのまま再利用する(ここで再代入・再検証しない)。

--root 未指定の場合のみ、以下でルート issue を新規作成する。 表のプレースホルダー行は scripts/merge-root-body.sh(Step 6 の --root 経路)が追記位置として読む。 文言を変えるときは同スクリプトの awk とテスト (n) を合わせて直す。

# MILESTONE が空でなければ --milestone を付与する(Step 2.5 で決定済み)
ROOT_ARGS=(--title "chore: 全 open issue の Phase 別トラッキング ($(date +%Y-%m-%d))")
if [[ -n "${MILESTONE}" ]]; then
  ROOT_ARGS+=(--milestone "${MILESTONE}")
fi

# gh issue create は issue URL を stdout に出力する(--json 非対応)。URL 末尾から番号を抽出する
ROOT_URL=$(gh issue create "${ROOT_ARGS[@]}" \
  --body "$(printf '\n' "${GRANULARITY}"; cat <<'EOF'
## 概要

全 open issue を Phase 別に 1 ツリーへ整理する。各 Phase 親 issue を sub-issues として紐付け。

## Phase 別実装計画

| Phase | 親 issue | 直下 | 総 open 件数 |
|-------|----------|------|-------------|
| (作成後に更新) | | | |

## 運用

- 新規 issue は起票時に Phase 親へ紐付ける
- 実行順は sub-issues リスト順が正
- closed 親の下に open issue を残置しない
- implement-issue-tree が post-order DFS で消化可能な構造を維持する
EOF
)")
ROOT_NUMBER=$(printf '%s' "${ROOT_URL}" | grep -oE '[0-9]+$')
echo "ルート issue: ${ROOT_NUMBER}"
Step 4: Phase 親 issue を作成して紐付ける

Phase ごとに親 issue を作成し、sub_issues API でルートへ紐付ける。 以下のスニペットは PHASE 変数で Phase 番号を切り替える。--phase 指定時はその番号を、 全 Phase 起票時は処理中の Phase 番号を設定する(タイトル・ラベルとも PHASE に追従させる)。

# 処理対象の Phase 番号(--phase 指定時はその番号、全 Phase 起票時はループ中の番号)
PHASE=1

# phase ラベルが存在しないリポジトリでは issue 作成が失敗するため、必ず事前作成する
# (作成済みの場合は失敗を無視して続行する)
gh label create "phase:${PHASE}" --color "0075ca" 2>/dev/null || true

# --root 再実行時の重複防止: ルート直下に同じ Phase の open な親が既にあれば再利用する
# closed な親は再利用しない(closed 親の下に open issue を残置しない運用ルールと整合させる)。
# phase ラベルだけでは同ラベルの一般 issue がルート直下に混在した場合に誤マッチするため、
# Phase 親のタイトル規約(feat(phase-N): 接頭辞)でも絞り込む。
# 直下が 100 件を超える場合に備えてページングで全件走査する
PHASE_NUMBER=""
PAGE=1
while true; do
  RESULT=$(gh api \
    "repos/{owner}/{repo}/issues/${ROOT_NUMBER}/sub_issues?per_page=100&page=${PAGE}")
  PHASE_NUMBER=$(echo "${RESULT}" | jq -r \
    "[.[] | select(.state == \"open\"
      and (.title | startswith(\"feat(phase-${PHASE}):\"))
      and any(.labels[]?; .name == \"phase:${PHASE}\"))][0].number // empty")
  COUNT=$(echo "${RESULT}" | jq 'length')
  if [[ -n "${PHASE_NUMBER}" || "${COUNT}" -lt 100 ]]; then break; fi
  PAGE=$((PAGE + 1))
done
[[ -n "${PHASE_NUMBER}" ]] && echo "既存の Phase 親 issue を再利用: #${PHASE_NUMBER}"

# 再利用する Phase 親が milestone 未設定の場合はここで揃える(milestone 導入前に
# 作られたツリーへの追記で、新規の子だけに milestone が付く不整合を防ぐ。冪等)
if [[ -n "${PHASE_NUMBER}" && -n "${MILESTONE}" ]]; then
  gh issue edit "${PHASE_NUMBER}" --milestone "${MILESTONE}"
fi

タイトル規約が feat(phase-N): と異なるツリーでは上記の自動判定に頼らず、候補をユーザーに 提示して再利用すべき Phase 親を確認する。

PHASE_NUMBER が空(既存の Phase 親がない)場合のみ、以下で新規作成してルートへ紐付ける。

# MILESTONE が空でなければ --milestone を付与する(Step 2.5 で決定済み)
PHASE_ARGS=(--title "feat(phase-${PHASE}): Phase ${PHASE} 基盤整備" --label "phase:${PHASE}")
if [[ -n "${MILESTONE}" ]]; then
  PHASE_ARGS+=(--milestone "${MILESTONE}")
fi

# Phase 親 issue を作成(URL 末尾から番号を抽出)
PHASE_URL=$(gh issue create "${PHASE_ARGS[@]}" \
  --body "$(cat <<'EOF'
## 概要

この Phase の実装タスクをまとめる親 issue。

## タスク一覧

| Issue | タイトル | 分解 |
|-------|---------|------|
| (子 issue 作成後に更新) | | |
EOF
)")
PHASE_NUMBER=$(printf '%s' "${PHASE_URL}" | grep -oE '[0-9]+$')

# ルートへ紐付け。sub_issue_id は issue 番号ではなく database id を渡す(GitHub sub-issues API 仕様)
PHASE_ID=$(gh api "repos/{owner}/{repo}/issues/${PHASE_NUMBER}" --jq '.id')
gh api \
  --method POST \
  "repos/{owner}/{repo}/issues/${ROOT_NUMBER}/sub_issues" \
  -F "sub_issue_id=${PHASE_ID}"
Step 5: 子 issue・sub-issue を作成して紐付ける

各タスクを issue として作成し、Phase 親へ紐付ける。粒度基準(既定 2h・--granularity)超のタスクはさらに sub-issue に分解する。

# MILESTONE が空でなければ --milestone を付与する(Step 2.5 で決定済み)
CHILD_ARGS=(--title "feat: タスク名" --label "phase:${PHASE}")
if [[ -n "${MILESTONE}" ]]; then
  CHILD_ARGS+=(--milestone "${MILESTONE}")
fi

# 子 issue を作成(URL 末尾から番号を抽出)。PHASE は Step 4 で設定した番号を引き継ぐ
CHILD_URL=$(gh issue create "${CHILD_ARGS[@]}" \
  --body "$(cat <<'EOF'
## 概要

...

## 受け入れ条件

- [ ] 条件1
- [ ] 条件2
EOF
)")
CHILD_NUMBER=$(printf '%s' "${CHILD_URL}" | grep -oE '[0-9]+$')

# Phase 親へ紐付け(sub_issue_id は database id)
CHILD_ID=$(gh api "repos/{owner}/{repo}/issues/${CHILD_NUMBER}" --jq '.id')
gh api \
  --method POST \
  "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues" \
  -F "sub_issue_id=${CHILD_ID}"

# 粒度基準(既定 2h・--granularity)超の場合は sub-issue を作成して子 issue へ紐付け(同じく MILESTONE を付与)
SUB_ARGS=(--title "feat: サブタスク名" --label "phase:${PHASE}")
if [[ -n "${MILESTONE}" ]]; then
  SUB_ARGS+=(--milestone "${MILESTONE}")
fi
SUB_URL=$(gh issue create "${SUB_ARGS[@]}" --body "...")
SUB_NUMBER=$(printf '%s' "${SUB_URL}" | grep -oE '[0-9]+$')

SUB_ID=$(gh api "repos/{owner}/{repo}/issues/${SUB_NUMBER}" --jq '.id')
gh api \
  --method POST \
  "repos/{owner}/{repo}/issues/${CHILD_NUMBER}/sub_issues" \
  -F "sub_issue_id=${SUB_ID}"

issue 数が多い場合(50 件超が目安)は per_page=100 パラメータを使用し、ページネーションで全件確認する。

# ページネーション例(全 sub-issues を取得)
PAGE=1
while true; do
  RESULT=$(gh api \
    "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues?per_page=100&page=${PAGE}")
  COUNT=$(echo "${RESULT}" | jq 'length')
  echo "${RESULT}"
  if [ "${COUNT}" -lt 100 ]; then break; fi
  PAGE=$((PAGE + 1))
done
Step 6:按 Phase 更新根 issue 正文中的表格

创建完所有 issue 后,使用实际 issue 编号和数量更新根 issue 正文中的 Phase 表格。

使用 --root 部分创建 issue 时,不要完全替换正文。 由于现有根 issue 正文中包含先前 Phase 的表格,因此应获取当前正文,仅追加或更新本次创建的 Phase 对应行和章节,然后通过 gh issue edit 更新正文。仅在新建时(未指定 --root)完全替换正文,并将生成工作交给 scripts/create-root-body.sh;--root 时的合并工作交给 scripts/merge-root-body.sh(不要将正文生成逻辑写入 SKILL.md)。 即使根 issue 仍是 Step 3 的模板(重新执行在到达 Step 6 前中断的创建操作),也要将表格中的占位符行 | (作成後に更新) | 替换为本次 Phase 行并完成操作。

指定 --root 时,scripts/merge-root-body.sh 会获取现有正文,并根据实际树生成本次 Phase 的行和章节,再进行合并(Issue #556)。由于各代码围栏可能在独立的 shell 中执行,此围栏会像 Step 3 / Step 4 一样,通过脚本从 3 种布局中自行解析依赖项。 如果 ROOT_NUMBER(根 issue 编号)、GRANULARITY(Step 1 中确定的粒度)、PHASE(Step 4 中确定的 Phase 编号 N)或 PHASE_NUMBER(该 Phase 父 issue 编号)未设置,则在启动前停止。

: "${ROOT_NUMBER:?ROOT_NUMBER 未設定}" "${GRANULARITY:?GRANULARITY 未設定}" \
  "${PHASE:?PHASE 未設定}" "${PHASE_NUMBER:?PHASE_NUMBER 未設定}"

MERGE_ROOT_BODY=""
for CANDIDATE in \
  "skills/create-issue-tree/scripts/merge-root-body.sh" \
  ".agents/skills/create-issue-tree/scripts/merge-root-body.sh" \
  ".claude/skills/create-issue-tree/scripts/merge-root-body.sh"; do
  # 存在確認は -f のみ(vendoring で実行ビットが落ちても bash 経由で起動できる)
  if [[ -f "${CANDIDATE}" ]]; then
    MERGE_ROOT_BODY="${CANDIDATE}"
    break
  fi
done
if [[ -z "${MERGE_ROOT_BODY}" ]]; then
  echo "エラー: merge-root-body.sh が見つからない(3 レイアウトいずれにも存在しない)" >&2
  exit 1
fi

# echo を最終コマンドにしない。非ゼロ終了はそのままフェンスの終了ステータスにする
status=0
bash "${MERGE_ROOT_BODY}" --root "${ROOT_NUMBER}" --granularity "${GRANULARITY}" \
  --phase "${PHASE}" --phase-number "${PHASE_NUMBER}" || status=$?
if (( status != 0 )); then
  echo "exit=${status}" >&2
  exit "${status}"
fi

退出代码(成功时,stdout 的最后一行是 result=merged root=<n> phase=<N>。错误消息输出到 stderr)

exit含义是否更新正文
0将本次 Phase 内容合并到现有正文,且 gh issue edit 成功是
1参数无效(--root、--phase-number 必须为正整数,--phase 必须为数字,--granularity 必须匹配 ^[1-9][0-9]*h$);获取现有正文、Phase 父 issue、sub_issues 或后代失败;没有追加位置、无法输出 Phase 行、仍有占位符或检查失败、创建临时文件失败否
2前提条件不满足(无法加载 tree-lib.sh,缺少 gh 或 jq)否
其他gh issue edit 自身的退出代码未知(需确认实际状态)

合并不变条件:仅根据实际树(sub_issues API)生成 Phase 行和章节,并保留先前 Phase 的表格及自由文本 / 将 issue 标题视为不可信数据,把 & < > 反引号、反斜杠和 | 转换为 HTML 字符引用,并将换行替换为空格 / 若正文中仍有占位符,则不执行 gh issue edit(fail-closed)/ 无论成功与否,都删除临时文件 / 始终使用 GRANULARITY 重写粒度标记 / 如果存在由 update-issue-tree 管理的标记区间,仅替换其内部内容;<details> 内部(存档部分)不作为目标 / 如果标记不成对、排除后仍有同名标题,或正文中的 <details> 未闭合,则中止(行内或行尾的 </details> 也算作闭合;但只有在 <details> 内开始的行、以未闭合状态结束的行,以及行首为无属性的 <details> / </details> 的行才会被排除〔原样保留〕;表格行或标题行中非行首的标签字符串仍是替换目标。表格行或标题行未闭合时也会中止。在管理标记区间内部〔若没有标记,则为整个正文;不包括代码围栏内〕,如果存在包含反引号、`` 或反斜杠的行,或包含未获允许的 HTML 标签的行〔移除无属性的 <details> </details> <summary> </summary> 后仍有 < 的行。带属性的 <details>、<span>、<br> 等均属于此类〕,由于无法判断标签是文本还是结构,会以 exit 1 中止。单独的 > 不会导致中止)。详情请参阅 scripts/merge-root-body.sh 开头的注释。

未指定 --root(新建)时,scripts/create-root-body.sh 会根据实际树生成正文并更新(Issue #555)。两条路径均共用 scripts/tree-lib.sh 中的辅助函数(如 list_subs)。

同样,如果 ROOT_NUMBER 和 GRANULARITY 未设置,则在启动前停止。

: "${ROOT_NUMBER:?ROOT_NUMBER 未設定}" "${GRANULARITY:?GRANULARITY 未設定}"

CREATE_ROOT_BODY=""
for CANDIDATE in \
  "skills/create-issue-tree/scripts/create-root-body.sh" \
  ".agents/skills/create-issue-tree/scripts/create-root-body.sh" \
  ".claude/skills/create-issue-tree/scripts/create-root-body.sh"; do
  # 存在確認は -f のみ(vendoring で実行ビットが落ちても bash 経由で起動できる)
  if [[ -f "${CANDIDATE}" ]]; then
    CREATE_ROOT_BODY="${CANDIDATE}"
    break
  fi
done
if [[ -z "${CREATE_ROOT_BODY}" ]]; then
  echo "エラー: create-root-body.sh が見つからない(3 レイアウトいずれにも存在しない)" >&2
  exit 1
fi

# echo を最終コマンドにしない。非ゼロ終了はそのままフェンスの終了ステータスにする
status=0
bash "${CREATE_ROOT_BODY}" --root "${ROOT_NUMBER}" --granularity "${GRANULARITY}" || status=$?
if (( status != 0 )); then
  echo "exit=${status}" >&2
  exit "${status}"
fi

退出代码(成功时,stdout 的最后一行是 result=updated root=<n> phases=<k>。错误消息输出到 stderr)

exit含义是否更新正文
0生成正文,且 gh issue edit 成功是
1参数无效(--root 必须为正整数,--granularity 必须匹配 ^[1-9][0-9]*h$);获取 sub_issues 失败;没有 Phase 父 issue;无法确定 Phase 编号;获取后代失败;仍有占位符或检查失败;Phase 行数不匹配;创建临时文件失败否
2前提条件不满足(无法加载 tree-lib.sh,缺少 gh 或 jq)否
其他gh issue edit 自身的退出代码未知(需确认实际状态)

生成不变条件:仅根据实际树(sub_issues API)生成表格 / 将 issue 标题视为不可信数据,把 & < > 反引号、反斜杠和 | 转换为 HTML 字符引用,并将换行替换为空格 / 若正文中仍有占位符,则不执行 gh issue edit (fail-closed)/ 无论成功与否,都删除临时文件。详情请参阅 scripts/create-root-body.sh 开头的注释。

Step 7:报告创建结果
## create-issue-tree 完了レポート

### ルート issue
- #N: タイトル

### Phase 別作成サマリー
| Phase | 親 issue | 起票数 |
|-------|----------|-------|
| Phase 1 | #N | N 件 |

### 作成した issue 一覧
- #N: タイトル(Phase 1 / 親: #M)
...

### 次のアクション
- 実装消化: implement-issue-tree スキルに ルート issue 番号を渡す
- ツリー更新: update-issue-tree スキルでトラッキング issue を棚卸しする

验证

  • 确认根 issue 的 sub-issues 中列出了各 Phase 父 issue
  • 确认各 Phase 父 issue 的 sub-issues 中列出了子 issue
  • 通过 gh issue view "${ROOT_NUMBER}" 确认根 issue 正文中的 Phase 表格已正确生成
  • 通过 gh issue view "${ROOT_NUMBER}" --json body --jq .body 确认其中不再有 <phaseN_number>、#N、(作成後に更新) 或未格式化的 N 单元格(如果仍有残留,则重新执行 Step 6)
  • 如果分配了 MILESTONE,通过 gh issue view <N> --json milestone --jq '.milestone.title' 确认根 issue、Phase 父 issue 和子 issue 的值均与 ${MILESTONE} 一致
# sub-issues は 1 ページ最大 100 件。100 件超のツリーは Step 5 のページネーション
# ループで全件確認する(以下は per_page=100 で先頭ページのみ。件数が 100 未満なら全件)

# ルート直下の sub-issues を確認
gh api "repos/{owner}/{repo}/issues/${ROOT_NUMBER}/sub_issues?per_page=100" --jq '.[].number'

# Phase 親直下の sub-issues を確認
gh api "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues?per_page=100" --jq '.[].number'

# phase ラベルの同期確認(Phase 親・子 issue にラベルが付いているか確認)
gh api "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues?per_page=100" \
  --jq '.[] | {number: .number, labels: [.labels[].name]}'

よくある失敗

問題回避策
--root を指定せず 2 回目の部分起票でルート issue が重複作成される2 回目以降は必ず --root <既存ルートissue番号> を渡す
sub_issue_id に issue 番号をそのまま渡すgh api .../issues/<number> --jq '.id' で database id を取得してから POST する
phase ラベルが存在しないリポジトリで issue 作成が失敗するStep 4 冒頭の gh label create "phase:${PHASE}" を必ず先に実行する
--root 追記時に既存ルートの milestone が未設定なのに気づかず milestone なしで起票してしまうリポジトリに milestone が存在する場合、Step 2.5 は継承結果が空ならユーザー確認フローへ自動的に合流する(確認で milestone を選ぶとルート issue にも反映される)。milestone が 1 件もない非運用リポジトリでは非運用ガードによる milestone なし起票が正常動作
closed 親の下に open issue が残置されるPhase 親を close する前に全子 issue の close を確認する
Step 6 の本文を手書きのプレースホルダーのまま gh issue edit してルート本文が雛形で上書きされる実ツリーから表を生成する scripts/create-root-body.sh(新規作成)・scripts/merge-root-body.sh(--root)を使う。検査ガードに引っかかったら本文を送らず中止される
Step 6 到達前に中断し、雛形のままのルートが残った--root <ルート番号> を付けて再実行する。プレースホルダー行は今回の Phase 行へ置き換わる
--granularity に 2 h・2・0h 等を渡して中断される正整数+h 形式(^[1-9][0-9]*h$。例: 2h・4h)で指定する

注意事項

  • 1 issue は粒度基準(既定 2h・--granularity で変更可)程度に収める。 超えると判断した場合は sub-issue に分解する
  • issue タイトルは Conventional Commits 形式を推奨(feat:・fix:・chore: 等)
  • --phase 指定で部分起票した場合、別 Phase の追加起票では 必ず --root <既存ルートissue番号> を渡す(Step 3 の新規作成をスキップして既存ツリーへ継ぎ足し、Step 6 も全置換せず既存本文へ差分追記する)。起票後は update-issue-tree に同じルート issue 番号を渡して棚卸しする
  • ページネーション: sub-issues が 100 件を超える場合は per_page=100&page=N でページングして全件取得する
  • Step 6 の本文生成は新規作成が scripts/create-root-body.sh、--root マージが scripts/merge-root-body.sh。本文生成ロジックを SKILL.md へ書き戻さない(肥大とヘルパー重複の再発防止)
  • シェルコマンドの変数は必ず "${var}" でクォートする(コマンドインジェクション対策)
  • gh issue create は --json 非対応。issue URL を stdout に出力するため、| grep -oE '[0-9]+$' で末尾の番号を抽出して変数に保持する
  • sub_issues API の sub_issue_id は issue 番号ではなく database id(GitHub 仕様)。gh api "repos/{owner}/{repo}/issues/<number>" --jq '.id' で id を取得してから POST する。番号をそのまま渡すと誤った issue を紐付ける/404 になる
  • phase ラベルは Step 4 冒頭の gh label create "phase:${PHASE}" --color "0075ca" で issue 作成より前に必ず作成する(作成済みリポジトリでは no-op)

评论

支持 Markdown