- Skill ID
- fandhe-ai/agent-cli-skills/update-issue-tree
- Publisher
- fandhe-ai
- Repository
- agent-cli-skills
- Installs
- 0
- Files
- 7
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
fandhe-ai/agent-cli-skills/update-issue-treeInstalls these files- SKILL.md
- scripts/reassign-lib.sh
- scripts/reassign-sub-issue.sh
- tests/lib/gh-stub.mjs
- tests/reassign-sub-issue.test.mjs
- tests/skill-invocation-block.test.mjs
- tests/skill-prerequisites.test.mjs
What this skill tells the agent
update-issue-tree
既存の Issue ツリーを棚卸しし、ルート issue 本文を最新状態に再生成する。 closed 親下に残置された open issue の付け替え・孤児の再配置・phase ラベルの同期を実施し、implement-issue-tree が post-order DFS で消化できる構造を維持する。
使い方
ルートのトラッキング issue 番号を引数として渡す。 --granularity オプションで粒度基準(1 issue に収める実装時間の上限)を指定できる。 2h・4h・1h のように正整数+時間単位(h)で指定する。優先順位は `--granularity` 明示 > ルート issue 本文の `<!-- granularity: Nh -->` マーカー > 既定 `2h` の順(create-issue-tree と同じ粒度基準・同じマーカー形式)。値は Step 1 で GRANULARITY として確定し、Step 2・Step 7 の判定に使う。棚卸し後に Step 8 が本文を再生成する際、 GRANULARITY の確定値でマーカー行を保持する。
update-issue-tree 42
update-issue-tree 42 --granularity 4h前提条件
ghCLI がインストールされ、認証済みであること(gh auth statusで確認)- 対象リポジトリへの Issue 書き込み権限があること
- 対象ツリーは単一リポジトリ内で完結していること。GitHub の sub-issues はリポジトリを 跨いで紐付けられるが、本スキルは cross-repository sub-issue を対象外とする。 親リポジトリへの書き込みは本スキルの前提条件(対象リポジトリへの書き込み権限)の 外側にあり、誤ったリポジトリの同番号 issue を操作する事故(PR #314 の P0)を構造的に 防ぐため。この契約の実装は
scripts/reassign-sub-issue.shの exit 2 による fail-closed(下記 Step 3 の終了コード表を参照)。exit 2 は gh/jq 不在・未認証・issue 取得失敗(解消可能な前提不備)とも共有するため、このケースだけ stderr にreason=cross-repository-parentの安定マーカー行が追加で出る(終了コード表参照) - 対象 issue の親が別リポジトリにある場合、
scripts/reassign-sub-issue.shがその issue を 処理できるのは、親リポジトリ側で親リンクが取り外されparent_issue_urlが null(どの親にも 紐付いていない)状態になってからである。cross-repository の親リンクの取り外しは 親リポジトリ側の操作であり、そのリポジトリへの書き込み権限を持つ担当者が行う。本スキルの範囲外 - 同一リポジトリ内での親の付け替えは本スキルの中核機能であり、上記の null 要求の対象外である。 Step 3 は承認済みの旧親を
--old-parentに渡して DELETE→POST を実行するため、事前にparent_issue_urlを null にしておく必要はない(孤児の再配置は Step 4 が--old-parent省略で扱う)
フロー
Step 1: ツリー全体を再帰取得する
ルート issue から sub_issues API を再帰的に呼び出し、全階層のツリー構造を取得する。 ページネーションを考慮し、per_page=100 で全件取得する。
ROOT_NUMBER="<ルート issue 番号>"
# --granularity の値は代入前に Claude 側でも同じ正規表現(^[1-9][0-9]*h$)で検証し、
# 不一致なら**このフェンスを実行せず**ユーザーに再指定を求める(未信頼値を生成済みシェルへ
# 埋め込まない)。優先順位は --granularity > ルートのマーカー > 既定 2h の順。
# --granularity で渡された時間を実際の値で代入する(実行時に Claude が置き換える)
# 例: --granularity 4h が指定された場合 → GRANULARITY_ARG="4h" / 未指定の場合は空文字
GRANULARITY_ARG="<--granularity で渡された時間(未指定なら空文字)>"
if [[ -n "${GRANULARITY_ARG}" ]]; then
GRANULARITY="${GRANULARITY_ARG}"
GRANULARITY_SOURCE="--granularity 引数"
else
# ルート本文の取得失敗とマーカー不在は分離する。取得失敗は「粒度を既定へ倒さず中止」する
# (gh issue view 自体の失敗を || true で握り潰すと、権限エラー・issue 不在等を
# 「マーカーなし」と誤読して既定 2h へサイレントに倒れてしまうため)
ROOT_BODY=$(gh issue view "${ROOT_NUMBER}" --json body --jq '.body') \
|| { echo "エラー: ルート issue #${ROOT_NUMBER} の本文を取得できません。粒度を既定へ倒さず中止します。"; exit 1; }
# 既存ルート本文の <!-- granularity: Nh --> マーカーから継承する。マーカーが無ければ既定 2h
# (|| true は grep のマーカー不在にのみ掛かる。gh issue view 自体の失敗は上で既に処理済み)
ROOT_GRANULARITY=$(printf '%s\n' "${ROOT_BODY}" \
| grep -oE '<!-- granularity: [1-9][0-9]*h -->' | head -1 | grep -oE '[1-9][0-9]*h' || true)
if [[ -n "${ROOT_GRANULARITY}" ]]; then
GRANULARITY="${ROOT_GRANULARITY}"
GRANULARITY_SOURCE="ルート issue #${ROOT_NUMBER} のマーカー"
else
GRANULARITY="2h"
GRANULARITY_SOURCE="既定値"
fi
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})"
# ルート直下の sub-issues を取得(ページネーション対応)
fetch_sub_issues() {
local PARENT="${1}"
local PAGE=1
while true; do
RESULT=$(gh api \
"repos/{owner}/{repo}/issues/${PARENT}/sub_issues?per_page=100&page=${PAGE}")
echo "${RESULT}"
COUNT=$(echo "${RESULT}" | jq 'length')
if [ "${COUNT}" -lt 100 ]; then break; fi
PAGE=$((PAGE + 1))
done
}
# ルートから再帰的にツリーを構築
fetch_sub_issues "${ROOT_NUMBER}"以降の Step は Step 1 で出力された GRANULARITY の値を使う(既定 2h と読み替えない)。
各 issue の state(open / closed)・ラベル・タイトルを記録してツリーマップを作成する。
Step 2: 棚卸し対象を特定する
取得したツリーマップを分析し、以下のケースを特定する。
| ケース | 対応方針 |
|---|---|
| closed 親の下に open issue が残置されている | 適切な open Phase 親へ付け替え |
| どの親にも紐付いていない孤児 issue がある | 該当 Phase 親へ紐付け(Phase が不明な場合はユーザーに確認) |
| phase ラベルが親と一致しない issue がある | ラベルを同期 |
| 既存 Phase に収まらない新規タスクがある | 新 Phase 親の新設を検討 |
実装時間が ${GRANULARITY}(Step 1 で確定。既定 2h)超の issue が分解されていない | sub-issue に分解(create-issue-tree と同じ粒度基準) |
| 対象 issue の親が別リポジトリにある(cross-repository sub-issue) | 本スキルの対象外。棚卸し対象から除外し、Step 9 の要確認事項へ記載する |
棚卸し対象の一覧をユーザーに提示し、方針確認を取ってから変更を実行する。
承認された対象を Step 3 / Step 4 が読む 2 つの計画配列へ落とす。 配列は必ず宣言する (対象 0 件でも空配列として宣言する)。未宣言のまま Step 3 / Step 4 へ進むと "${REASSIGN_PLAN[@]}" が空展開され、承認済みの対象があってもエラーなく 0 件で完走して 完了報告まで進んでしまうため、両ステップの冒頭で宣言の有無を検査して fail-closed で停止する。
# 承認された「closed 親下の付け替え」対象。要素は "<issue> <old-parent> <new-parent>"。
# 空白区切りの 1 行 1 件にするのは、Step 3 のループが read で分割して受け取るため。
# issue 番号は全て 10 進の正整数であり、空白・改行を含まないことを Step 1 の取得時に保証する。
REASSIGN_PLAN=(
# "123 456 789"
)
# 承認された「孤児の再配置」対象。要素は "<orphan-issue> <phase-parent>"。
ORPHAN_PLAN=(
# "234 789"
)対象が 0 件の場合も上記のとおり空配列として宣言する。「対象なし(空配列)」と 「計画未設定(未宣言)」は意味が異なり、後者は Step 2 の実行漏れであって正常系ではない。
Step 3: closed 親下の残置 open issue を付け替える
closed 親の下に残置されている open issue を、対応する open Phase 親へ移動する。 「旧親から DELETE → 新親へ POST」の 2 段操作と、その前後の冪等性判定・事後確認は scripts/reassign-sub-issue.sh に集約されている(Issue #297。旧方式は SKILL.md 本文に 素の gh api を並べていたため、DELETE 失敗検知なしに POST へ進む等の欠陥があった)。 その呼び出し側で必要な共有処理(計画配列の存在・型検証、1 件呼び出しの終了ステータス解釈)は scripts/reassign-lib.sh に切り出してあり、Step 3 / Step 4 がそれぞれ source して使う。
各コードフェンスは独立したシェルで実行され得る(reassign-sub-issue.sh の設計前提と同じ)。 そのため共有処理をどちらかのフェンス内で定義して他方が再利用する構成にはしない。Step 4 の 実行時に関数定義が継承されず command not found となり、孤児の再配置が一切実行されないため (Issue #372 / PR #374 codex-review P1 第 3 ラウンド)。両 Step のフェンスは同一の前置きで reassign-lib.sh を自己完結的に解決・source する。
このスキルの配置ルートは導入形態(本リポジトリのソース/npx skills add による vendoring/.claude/skills/ symlink 経由)で異なる。source 前に 3 レイアウトを順に確認し、 実在するものを採用する(implement-issue-tree の scriptPath 3 レイアウト・contribute-skill の LOCAL_SKILL_DIR 解決と同じ考え方)。reassign-sub-issue.sh のパスはここでは指定しない—— ライブラリが自身の実体位置から兄弟として解決するため、どの導入形態でも対の実体が必ず組み合う。
# reassign-lib.sh を 3 レイアウトから解決して source する。
# **この前置きは Step 3 / Step 4 の両フェンスが同一の内容で持つ。** 手順書のコードフェンスは
# ブロックごとに独立シェルで実行され得るため、片方のフェンスで定義した関数・変数がもう一方へ
# 継承される保証がない(Issue #372 / PR #374 codex-review P1)。
REASSIGN_LIB=""
for CANDIDATE in \
"skills/update-issue-tree/scripts/reassign-lib.sh" \
".agents/skills/update-issue-tree/scripts/reassign-lib.sh" \
".claude/skills/update-issue-tree/scripts/reassign-lib.sh"; do
# 存在確認は -f のみで行う(-x にすると、npx skills add 等の vendoring で
# 実行ビットが落ちたファイルを「存在しない」と誤検知し、3 レイアウトいずれにも
# 見つからないという誤ったエラーメッセージになる)
if [[ -f "${CANDIDATE}" ]]; then
REASSIGN_LIB="${CANDIDATE}"
break
fi
done
if [[ -z "${REASSIGN_LIB}" ]]; then
echo "エラー: reassign-lib.sh が見つからない(3 レイアウトいずれにも存在しない)" >&2
exit 1
fi
# ライブラリは対の reassign-sub-issue.sh の存在まで確認したうえで、reassign_one /
# require_plan_array と REASSIGN_SCRIPT を定義する。欠落していれば非ゼロを返すので中断する。
# このフェンスで set -euo pipefail を使ってはならない(理由は reassign-lib.sh 冒頭のコメント参照。
# set -e があると reassign_one 内の `status=$?` 退避に到達せず、Issue #335 の欠陥が再発する)
source "${REASSIGN_LIB}" || exit 1
# 計画配列の存在・型ガード(fail-closed)。詳細は reassign-lib.sh の require_plan_array を参照
require_plan_array REASSIGN_PLAN || exit 1
# 呼び出し側ループ。契約の主体はここにある——「(b) 恒久的な対象外は次の 1 件へ進み、
# (a) 解消可能な前提不備は原因解消まで中断する」を実際に実現するのはこのループである。
# ISSUE_NUMBER / OLD_PARENT / NEW_PARENT は Step 2 で構築した REASSIGN_PLAN から供給する。
SKIPPED=()
NEEDS_REVIEW=()
for ENTRY in "${REASSIGN_PLAN[@]}"; do
# ENTRY は "<issue> <old-parent> <new-parent>" 形式。
# zsh は未クォートの展開を単語分割しないため、read で明示的に分割する
IFS=' ' read -r ISSUE_NUMBER OLD_PARENT NEW_PARENT <<< "${ENTRY}"
# `if reassign_one ...; then ... fi` の形にしてはならない。条件が偽で else が無い場合、
# `fi` の直後の $? は 0 になり(実測済み)、失敗が「成功」として読まれて
# (a)/(b)/(c) の判定も中断もすべて素通りする。`|| status=$?` で明示的に退避する。
status=0
reassign_one --issue "${ISSUE_NUMBER}" \
--old-parent "${OLD_PARENT}" \More from fandhe-ai
Use this skill in a project
Open any RiverX project, search the Skills panel for fandhe-ai/agent-cli-skills/update-issue-tree, and install it in one click.
