update-issue-tree
既存の GitHub Issue ツリーを棚卸し・更新するスキル。「ツリーを棚卸しして」「イシューツリーを更新して」「トラッキング issue を整理して」で使用。 ルートのトラッキング issue 番号を受け取り、sub_issues API でツリー全体を再帰取得 → closed 親下の残置 open issue 付け替え・孤児の再配置・新 Phase 親の新設・phase ラベル同期 → ルート issue 本文は既存本文を保持し、Phase 別表・棚卸し履歴の管理ブロックのみ更新する。 ツリー新規作成は create-issue-tree、実装消化は implement-issue-tree を参照。
How do I install this agent skill?
npx skills add https://github.com/fandhe-ai/agent-cli-skills --skill update-issue-treeIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides a suite of tools for managing GitHub issue hierarchies (issue trees). It recursively analyzes issue structures, reassigns sub-issues between parents, and updates a tracking issue with implementations of robust error handling and input sanitization. The skill includes extensive automated tests and focuses on maintaining tree consistency during complex operations.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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 の確定値でマーカー行を先頭に 1 行だけ保持する。
update-issue-tree 42
update-issue-tree 42 --granularity 4h
前提条件
ghCLI がインストールされ、認証済みであること(gh auth statusで確認)- 対象リポジトリへの Issue 書き込み権限があること
- 対象ツリーは単一リポジトリ内で完結していること。GitHub の sub-issues はリポジトリを
跨いで紐付けられるが、本スキルは cross-repository sub-issue を対象外とする。
親リポジトリへの書き込みは本スキルの前提条件(対象リポジトリへの書き込み権限)の
外側にあり、誤ったリポジトリの同番号 issue を操作する事故を構造的に防ぐため。この契約の実装は
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 に集約されている(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 となり、孤児の再配置が一切実行されないため。両 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}" \
--new-parent "${NEW_PARENT}" || status=$?
if (( status == 0 )); then
continue
fi
if (( status == 9 )); then
# (b) cross-repository 親。棚卸し対象から除外して次へ(Step 9 の要確認事項へ記載)
SKIPPED+=("${ISSUE_NUMBER}")
continue
fi
if (( status == 10 )); then
# (c) 補償復旧が成功し旧親へ復帰済み(本来の付け替えは未達)。fatal にせず要確認事項へ
# 記録して次の 1 件へ進む。exit 10 を中断扱いにすると残りの REASSIGN_PLAN が未処理のまま
# 中断し、Step 4 の孤児処理・Step 9 の報告経路自体がスキップされてしまう
NEEDS_REVIEW+=("#${ISSUE_NUMBER}: exit=10 restored — 旧親 #${OLD_PARENT} へ復帰済み。新親 #${NEW_PARENT} への付け替えは未達")
continue
fi
if (( status == 11 )); then
# (c) DELETE 後、第三者が別親を設定済みのため補償せず見送り。同一コマンドでの再実行禁止を
# 記録に残したうえで、fatal にせず次の 1 件へ進む
NEEDS_REVIEW+=("#${ISSUE_NUMBER}: exit=11 third-party-parent — 旧親 #${OLD_PARENT} から DELETE 済み、第三者が別親を設定済み。同じコマンドで再実行禁止。stderr の実測親を確認しユーザー承認のうえ再実行すること")
continue
fi
# (a) それ以外(exit 1-8。9・10・11 を除く)は握り潰さず中断する。原因を解消してから再実行する
echo "エラー: #${ISSUE_NUMBER} の付け替えが exit ${status} で失敗した。中断する" >&2
exit "${status}"
done
if (( ${#SKIPPED[@]} > 0 )); then
echo "対象外(cross-repository 親): ${SKIPPED[*]}" >&2
fi
if (( ${#NEEDS_REVIEW[@]} > 0 )); then
echo "要確認事項(Step 9 のレポートへ転記すること):" >&2
printf ' %s\n' "${NEEDS_REVIEW[@]}" >&2
fi
reassign_one は1 件分の呼び出しであり、非ゼロ終了は当該 1 件の失敗として
関数の返り値へ伝播する。判定に必要な stderr は関数内で捕捉したうえで必ず再出力するため、
診断情報は失われない。呼び出し側ループは Step 9 の要確認事項へ記録したうえで、次の 3 通りに
振り分ける。(a) 解消可能な前提不備(exit 2 のうち gh/jq 不在・未認証・issue 取得失敗等。
9・10・11 を除く exit 1〜8 全般)は原因解消まで中断する。(b) 恒久的な対象外
(cross-repository 親。exit 9)は要確認事項へ記載して棚卸し対象から除外し、次の 1 件の
呼び出しへ進む。(c) 部分的に変更済みだが処理は継続してよいもの(exit 10 = DELETE 後の
補償復旧が成功し旧親へ復帰済み・exit 11 = DELETE 後に第三者が別親を設定済みで補償を見送り)は
要確認事項へ記録したうえで、fatal にせず次の 1 件へ進む。exit 10 / 11 を即 exit で中断すると
残りの REASSIGN_PLAN が未処理のまま止まり、Step 4 の孤児処理・Step 9 の報告経路自体が
スキップされる欠陥になるため、この 2 コードはループを止めない(終了コード表を参照)。
(a)/(b) の判定は stderr に reason=cross-repository-parent が出ているかで機械的に行う
(無ければ (a))。(c) は返り値がそのまま 10 / 11 であるため、判定に stderr のマーカーは不要。
(c) だけを継続対象にする理由(exit 3/4/5/7/8 は継続しない): 終了コード表を見ると
exit 3・4・5・7・8 も「呼び出し側の扱い」欄に「要確認事項へ記載」と書かれており、一見
(c) と同じに見える。しかし止める・止めないの分岐点は「要確認事項へ記載するか」ではなく
当該 1 件の終端状態が実測で確定しているかである。exit 10 / 11 は補償復旧ルーチンが
実状態を再取得した末に「旧親配下」「第三者親配下」という確定した終端状態へ着地しており、
次の 1 件へ進んでも新たな破壊は生じない。対して exit 3(DELETE 失敗)・4(POST 失敗)・
5(事後確認不一致)・7(POST 時点のレース)・8(補償も失敗し状態不明/孤児)は、無変更・
孤児化・状態不明のいずれかであり、原因(gh 認証切れ・API 障害・レース条件)が
後続の全件に共通して波及する可能性が高いため、1 件で切り上げて原因究明を優先する。
この境界線は exit 10 / 11 に限定したものであり、3/4/5/7/8 へ安易に拡張しない。
引数
| 引数 | 必須 | 意味 |
|---|---|---|
--issue | 必須 | 付け替え対象の issue 番号 |
--new-parent | 必須 | 付け替え先の issue 番号。DELETE の前に新親を GET し、存在すること・--issue 自身でないこと(自己参照)・対象 issue と同一リポジトリにあることを検証する。いずれかを満たさない場合はDELETE を 1 件も撃たずに exit 1(自己参照)/ exit 2(存在しない・別リポジトリ)で無変更終端する |
--old-parent | 任意 | 現在の親。Step 2 でユーザーが承認した旧親を渡す。実測した現在の親と食い違う場合は何も変更せず exit 6 で停止する(承認外の親子関係を壊さないため)。省略時は「孤児である」ことを承認した意味になり、実測で親が居れば同じく exit 6 で停止する |
--repo | 任意 | owner/name。対象 issue(--issue)の所在であり、親(--old-parent / --new-parent)の所在ではない。省略時は cwd の git remote から解決。--repo は対象 issue の GET だけでなく DELETE / POST を含む全 API パスを切り替えるため、親が別リポジトリにあるからといって親リポジトリの値を入れてはならない(入れると親リポジトリ側の同番号 issue を誤操作する) |
終了コードと result= 行
stdout 最終行が result=<state> issue=<n> new_parent=<n> old_parent=<n|-> の形式で
機械可読な内訳を返す。非ゼロ終了は 1 件も握り潰さず、Step 9 の完了レポートの
「要確認事項」へ必ず記載する。
| 終了コード | state | 意味 | 呼び出し側の扱い | 変更の有無 |
|---|---|---|---|---|
| 0 | reassigned | DELETE→POST を実施 | 「付け替え」件数へ計上 | 変更あり(成功) |
| 0 | already-attached | 既に新親配下(no-op) | 件数へ計上しない | 無変更(no-op) |
| 0 | posted-only | 旧親配下になく POST のみ | 「孤児の再配置」件数へ計上(Step 4 と同一スクリプト) | 変更あり(POST のみ) |
| 1 | — | 引数・使い方エラー(--new-parent の自己参照を含む) | 実行者の誤り。修正して再実行 | 無変更(API 未実行) |
| 2 | — | 前提不備。2 類型が混在し、stderr のマーカー行で機械的に判定する: reason=cross-repository-parent が出ていれば (b)、無ければ (a)。(a) 解消して再実行できるもの(gh/jq 不在・未認証・issue 取得失敗・新親が存在しない・新親が Pull Request・新親が別リポジトリ)と、(b) 恒久的に対象外の cross-repository 親(同一コマンドの再実行では解決しない。親リポジトリ側で親リンクが外れるまで本スキルでは処理できない) | (a) は原因を解消して再実行。(b) は要確認事項へ記載し、棚卸し対象から除外する(Step 2 参照) | 無変更(DELETE / POST 未実行) |
| 3 | — | DELETE 失敗(gh の非ゼロ終了)。POST は実行していない。 DELETE の応答だけを信頼せず実状態を再取得・安定確認したうえで、実測でも旧親配下のままであることを確認して初めて確定する(応答取得だけが失敗しサーバー側では成立していた場合は孤児として POST 工程へ進むため、この exit には到達しない) | 要確認事項へ記載。旧親配下のまま | 無変更(実測で確認済み。DELETE 失敗・旧親配下のまま) |
| 4 | — | POST 失敗(孤児経路のみ。DELETE を伴わないため孤児化リスク自体は無い)。POST の応答だけを信頼せず実状態を再取得・安定確認したうえで、実測でも孤児のままであることを確認して初めて確定する(応答取得だけが失敗しサーバー側では成立していた場合は posted-only として exit 0 になるため、この exit には到達しない) | 要確認事項へ記載 | 無変更(実測で確認済み。DELETE 未実行) |
| 5 | — | 事後確認で新親配下に見つからない(別リポジトリの親配下にある場合を含む) | 要確認事項へ記載。手動で実状態を確認 | 変更あり(DELETE / POST は実行済み・実状態の手動確認が必要) |
| 6 | — | 承認された旧親と実測が食い違う。何も変更していない | 要確認事項へ記載。同じコマンドで再実行してはならない。 stderr が示す実測の親をユーザーへ提示して承認を得たうえで、--old-parent にその値を入れて再実行する | 無変更 |
| 7 | — | POST 時点で別の親が付いていたレース。DELETE 未実行のため無変更 | 要確認事項へ記載。実測し直して承認を取り直したうえで再実行する | 無変更(DELETE 未実行) |
| 8 | — | 次の 3 系統いずれかの実状態再取得・安定確認が孤児のまま/状態不明/第三者の親配下で終端した: (1) DELETE 後の POST 失敗に対する補償復旧(補償 POST 自体の失敗・復旧用の再取得失敗・補償後の検証不一致・反映遅延の再確認不一致のいずれか。reason=compensation-post-failed(実測でも孤児)/ reason=compensation-post-failed-third-party-parent(補償 POST 失敗中に第三者が別の親を設定済みと実測)で内訳を判別)、(2) DELETE 失敗時の実状態再取得(DELETE はエラー応答だったがサーバー側で成立していた可能性があるため、応答だけで exit 3 と断定せず再取得した結果、再取得自体の失敗・不一致・不安定だった)、(3) 孤児経路の POST 失敗時の実状態再取得(POST はエラー応答だったがサーバー側で成立していた可能性があるため、応答だけで exit 4 と断定せず再取得した結果、再取得自体の失敗・不一致・不安定だった)。(2)(3) はいずれも reason=recovery-state-unknown を出す。(1) の再取得系失敗も同じ reason=recovery-state-unknown を出す | 要確認事項へ記載。いずれも無変更ではない(実測で確定できなかった状態不明)。 実状態を確認し必要なら手で紐付け直す。同一コマンドの再実行では回復しない | (1) 部分変更(旧親から外れ、新親にも付いていない。第三者配下の場合を除く)。(2)(3) 状態不明(DELETE / POST がサーバー側で成立していたか未確定のため、無変更とも部分変更とも断定しない) |
| 10 | restored | DELETE 後の POST 失敗に対する補償復旧が成功し、実測で旧親配下へ復帰した(本来の目的だった新親への付け替えは未達) | 要確認事項へ記載(本来の付け替えが未達のため)。件数へは計上しない | 変更あり(旧親へ復帰。新親への付け替えは不成立) |
| 11 | — | 実状態を再取得したところ、第三者が別の親を設定済みだったため補償せず見送った(reason=third-party-parent)。次のいずれかから到達する: (a) DELETE 後の POST 失敗を受けての再取得、(b) DELETE 失敗そのものを受けての再取得(DELETE の応答だけで exit 3 と断定せず再取得した結果、第三者の親配下と判明した場合)。新親配下の場合はこの判定より先に成功経路(exit 0 / result=reassigned)へ倒すため、この exit 11 に到達するのは実測が旧親でも新親でもない別の親だった場合に限る | 要確認事項へ記載。同じコマンドで再実行してはならない。 stderr が示す実測の親をユーザーへ提示して承認を得たうえで再実行する | 変更あり(旧親からは既に DELETE 済み。「補償 POST を撃たず fail-closed で停止」は新親への POST 側の話であり、DELETE は exit 11 に到達する時点で既に成立している(第三者の親配下に見えている以上、サーバー側のデータモデル上、旧親からは既に外れている)。 実測では第三者が設定した別親配下にある) |
スクリプト自身は 0〜8・10・11 を返す(9 は使わない)。 reassign-lib.sh の
reassign_one ラッパは、これに加えて 9 = 恒久的に対象外(cross-repository 親) を返す。9 はスクリプトの終了コードではなく、ラッパが stderr の
reason=cross-repository-parent マーカーを判定して合成する値であり、呼び出し側ループが
「棚卸し対象から除外して次の 1 件へ進む」のシグナルとして使う。マーカーの判定には
スクリプトの stderr が必要なため、ラッパは stderr をファイルへ捕捉したうえで必ず再出力する
(診断情報は握り潰さない)。10・11 はラッパの「その他はスクリプトの終了コードをそのまま返す」
契約により無加工で透過するため、reassign-lib.sh 側の変更は不要。
DELETE 後の POST 失敗時の補償復旧の契約: 新親の事前検証は「事前に判定できる 拒否条件」を DELETE 前に潰すが、DELETE と POST の間のレース・一時的な 5xx は事前判定できない 残余として残る。POST 失敗で即終端すると対象 issue が孤児のまま滞留するため、POST 失敗を 検知した時点で対象 issue の実状態を再取得してから分岐する(実測を先に確定させてから動く):
- 実測の親が新親 → POST は偽陰性だった。通常の成功終端(
exit 0/result=reassigned)と同じ扱い。 ただし旧親・孤児を期待した再確認の途中で新親を初めて観測した場合は、新親を期待値として もう 1 回取得し、2 回連続で新親配下が観測できたときにだけreassignedを確定する。 一致しない・再取得に失敗した場合は成功へ倒さずexit 8(状態不明)で終端する - 実測で孤児 → 1 回の読み取りだけでは DELETE/POST の反映遅延による過渡状態を見ている
可能性を排除できないため、補償 POST という書き込みを行う前に短い間隔を空けて再取得し、
2 回連続で親なしが観測できて初めて安定した孤児として確定する。安定確認できれば、承認済み操作の逆操作
(旧親へ戻す)は承認範囲内のため補償 POST を撃つ。成功なら事後確認を取り直し
exit 10/result=restored。補償 POST 自体が失敗する多重障害、または再取得・事後確認・ 安定確認が失敗する状態不明ケースは書き込まずにexit 8で終端する - 実測で第三者が別の親(別リポジトリを含む)を設定済み → 補償 POST を撃たず
exit 11で fail-closed 停止する。第三者が確定させた親子関係を上書きしない
DELETE 失敗・孤児経路 POST 失敗の実状態再確認: 上記は DELETE 成功後の
POST 失敗(部分変更)に対する補償復旧だが、DELETE 自体の失敗(reason 系マーカーを持たない
gh の非ゼロ終了)と孤児経路(DELETE を伴わない)の POST 失敗も同様に扱う。
gh api は終了コード 0 で返しても応答取得だけが失敗することがある一方、非ゼロ終了は
「サーバー側で処理が成立しなかった」ことを保証しない(応答取得側の失敗も非ゼロになり得る)。
このため DELETE / POST の非ゼロ終了だけを根拠に exit 3 / exit 4(無変更)と即断すると、
実際にはサーバー側で成立していた場合に誤報になる。
対称に実状態を再取得してから分岐する:
- DELETE 失敗 → 実状態を再取得し、旧親配下のまま安定確認できれば
exit 3(無変更)。孤児であることを安定確認できれば DELETE はサーバー側で成立済みとみなし、 そのまま POST 工程(新親への付け替え)へ進む。新親配下であることを安定確認できれば、 第三者親判定より先に判定し、DELETE→POST が既に成立していた(または並行実行が承認済みの 新親へ既に付け替え済みだった)とみなして既存の成功経路と同じresult=reassignedでexit 0にする(最初の読み取りで直接新親を観測した場合も、安定確認の途中で新親を観測した 場合と同じ成功終端にそろえ、観測タイミングだけで承認外の第三者親(exit 11)へ倒れないようにする)。それ以外で第三者が別の親を設定済みならexit 11reason=third-party-parent(既存の補償復旧経路と同じ扱い)。再取得の失敗・ 不一致・不安定はexit 8reason=recovery-state-unknown - 孤児経路(DELETE なし)の POST 失敗 → 実状態を再取得し、孤児のまま安定確認できれば
exit 4(無変更)。新親配下であることを安定確認できれば POST は偽陰性だった とみなしresult=posted-onlyでexit 0。この経路には DELETE が無く孤児化リスク自体が 無いため、補償の書き込み(POST)は発生しない(判定のみで完結する)。エラーメッセージ ベースのレース検知(exit 7。"only have one parent")は再取得より先に判定する。それ以外 (再取得の失敗・不一致・不安定・メッセージ非該当の第三者親)はexit 8reason=recovery-state-unknownで終端する
事前検証と事後報告の契約: 事前に判定できる拒否条件(新親の不存在・自己参照・ 別リポジトリ)は DELETE を撃たずに exit 1 / 2 で無変更終端する。一方、事前に判定できない 拒否条件(事前 GET と DELETE / POST の間に第三者が状態を動かすレース、循環参照、新親が sub-issue を受け付けない状態など)は exit 4 / exit 7 / exit 8 として事後に報告する 契約であり、事前検証はこれを置き換えない。DELETE 後の POST 失敗(孤児化しうる部分変更) だけは exit での即報告ではなく、上記の補償復旧ルーチンを経由してから exit 8 / 10 / 11 の いずれかへ着地する。
GET 回数: 経路ごとの GET 回数は次のとおり(付け替え・再配置の経路は新親の事前検証の 1 回を含む)。
| 経路 | GET 回数 |
|---|---|
| 正常な付け替え(reassigned) | 3 |
| 孤児の再配置(posted-only) | 3 |
| already-attached(no-op) | 1 |
| 承認不一致(exit 6) | 1 |
事前検証の GET は最大 1 回で、update-issue-tree の 1 ラン当たりの呼び出し回数は数十件規模。
認証済み REST の 5,000 req/h に対して無視できる。対して防ぐのは不可逆な孤児化であり、
割に合う。孤児経路(DELETE を伴わず孤児化リスク自体は無い経路)にも検証を通しているのは、
「事前に判定できる拒否条件は必ず無変更で終端する」契約を経路によらず統一する意図的な判断
であり、新親が存在しない場合は孤児経路でも exit 2 になる。
Step 4: 孤児 issue を再配置する
どの親にも紐付いていない孤児 issue を適切な Phase 親へ紐付ける。
--old-parent を省略して同じスクリプトを呼ぶ(DELETE を飛ばして POST のみ実行される)。
Phase が不明な issue はタイトル・本文を読んで判断し、判断できない場合はユーザーに確認する。
このフェンスは Step 3 の実行結果に依存せず単体で実行できる。 フェンスは独立シェルで 実行され得るため、Step 3 が定義した関数・変数を継承しない前提で書く。前置きは Step 3 と同一である。
# 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
# Step 3 と同じガードを同じ関数で行う(非対称を作らない)
require_plan_array ORPHAN_PLAN || exit 1
SKIPPED_ORPHANS=()
NEEDS_REVIEW_ORPHANS=()
for ENTRY in "${ORPHAN_PLAN[@]}"; do
# ENTRY は "<orphan-issue> <phase-parent>" 形式
IFS=' ' read -r ORPHAN_NUMBER PHASE_NUMBER <<< "${ENTRY}"
# Step 3 と同じ理由で `if ...; then ... fi` は使わない(`fi` 直後の $? は 0 になる)
status=0
reassign_one --issue "${ORPHAN_NUMBER}" --new-parent "${PHASE_NUMBER}" || status=$?
if (( status == 0 )); then
continue
fi
if (( status == 9 )); then
SKIPPED_ORPHANS+=("${ORPHAN_NUMBER}")
continue
fi
if (( status == 10 )); then
# Step 3 と同じ (c) 扱い。孤児経路は --old-parent を渡さないため通常は起こり得ないが、
# ループ構造を Step 3 と非対称にしないため同じ分岐を持つ
NEEDS_REVIEW_ORPHANS+=("#${ORPHAN_NUMBER}: exit=10 restored — 補償復旧が発生。Phase 親 #${PHASE_NUMBER} への紐付けは未達。実状態を確認すること")
continue
fi
if (( status == 11 )); then
NEEDS_REVIEW_ORPHANS+=("#${ORPHAN_NUMBER}: exit=11 third-party-parent — 第三者が別親を設定済み。同じコマンドで再実行禁止。stderr の実測親を確認しユーザー承認のうえ再実行すること")
continue
fi
echo "エラー: #${ORPHAN_NUMBER} の再配置が exit ${status} で失敗した。中断する" >&2
exit "${status}"
done
if (( ${#SKIPPED_ORPHANS[@]} > 0 )); then
echo "対象外(cross-repository 親): ${SKIPPED_ORPHANS[*]}" >&2
fi
if (( ${#NEEDS_REVIEW_ORPHANS[@]} > 0 )); then
echo "要確認事項(Step 9 のレポートへ転記すること):" >&2
printf ' %s\n' "${NEEDS_REVIEW_ORPHANS[@]}" >&2
fi
Step 3 と同一の関数・同一のループ構造であり、終了コードの扱いも同じ
((a) 解消可能な前提不備のみ原因解消まで中断、(b) 恒久的な対象外(exit 9)は要確認事項へ記載して
次の 1 件へ進む、(c) 部分的に変更済みだが継続してよいもの(exit 10 / 11)も要確認事項へ記録して
fatal にせず次の 1 件へ進む。(b) の判定は関数が捕捉した stderr の
reason=cross-repository-parent マーカーで機械的に行い、返り値 9 として呼び出し側へ伝える。
終了コード表を参照)。
Step 5: 必要に応じて新 Phase 親を新設する
既存 Phase に収まらない新規タスクが多い場合、新 Phase 親 issue を作成してルートへ紐付ける。
(この POST は reassign-sub-issue.sh を使わない。たった今作成した、親を持たないことが
自明な issue への単発 POST であり、DELETE パス・冪等性判定の対象外のため)
# phase ラベルが存在しないリポジトリでは issue 作成が失敗するため、必ず事前作成する
# (作成済みの場合は失敗を無視して続行する)
gh label create "phase:N" --color "0075ca" 2>/dev/null || true
# gh issue create は URL を出力する(--json 非対応)。URL 末尾から番号を抽出する
NEW_PHASE_URL=$(gh issue create \
--title "feat(phase-N): Phase N タイトル" \
--label "phase:N" \
--body "$(cat <<'EOF'
## 概要
Phase N の実装タスクをまとめる親 issue。
## タスク一覧
| Issue | タイトル | 分解 |
|-------|---------|------|
EOF
)")
NEW_PHASE_NUMBER=$(printf '%s' "${NEW_PHASE_URL}" | grep -oE '[0-9]+$')
# ルートへ紐付け。sub_issue_id は issue 番号ではなく database id を渡す(GitHub sub-issues API 仕様)
NEW_PHASE_ID=$(gh api "repos/{owner}/{repo}/issues/${NEW_PHASE_NUMBER}" --jq '.id')
gh api \
--method POST \
"repos/{owner}/{repo}/issues/${ROOT_NUMBER}/sub_issues" \
-F "sub_issue_id=${NEW_PHASE_ID}"
Step 6: phase ラベルを同期する
各 issue の phase ラベルが親 Phase と一致しているか確認し、不一致のラベルを修正する。
# ラベルを追加
gh issue edit "${ISSUE_NUMBER}" --add-label "phase:1"
# 古いラベルを削除
gh issue edit "${ISSUE_NUMBER}" --remove-label "phase:0"
Step 7: 粒度基準超の issue を sub-issue に分解する
棚卸し中に実装時間が ${GRANULARITY}(Step 1 で確定。既定 2h)超と判断した issue は、create-issue-tree と同じ粒度基準で sub-issue に分解する。
(この POST も reassign-sub-issue.sh を使わない。理由は Step 5 と同じ: 新規作成した
親なし issue への単発 POST)
# phase ラベルが存在しない場合に備えて事前作成する(作成済みなら no-op)
gh label create "phase:N" --color "0075ca" 2>/dev/null || true
# sub-issue を作成(URL 末尾から番号を抽出)
SUB_URL=$(gh issue create \
--title "feat: サブタスク名" \
--label "phase:N" \
--body "...")
SUB_NUMBER=$(printf '%s' "${SUB_URL}" | grep -oE '[0-9]+$')
# 親 issue へ紐付け(sub_issue_id は database id)
SUB_ID=$(gh api "repos/{owner}/{repo}/issues/${SUB_NUMBER}" --jq '.id')
gh api \
--method POST \
"repos/{owner}/{repo}/issues/${ISSUE_NUMBER}/sub_issues" \
-F "sub_issue_id=${SUB_ID}"
Step 8: ルート issue 本文の管理ブロックを更新する(既存本文は保持)
ルート issue 本文のうち本スキルが管理する範囲だけを実ツリーから再生成して差し替える。
それ以外の本文(概要・運用・手書きの計画や判断根拠・追記)は 1 行も失わない。本文全体を
固定テンプレートで書き戻してはならない(人が書いた内容が棚卸しのたびに消え、件数・日付の
プレースホルダーが本文へ到達し得るため。Issue #545)。処理は scripts/update-root-body.sh
に集約されている。
管理範囲と不変条件
| 領域 | マーカー(行全体で一致・コードフェンス外のみ有効) | 更新規則 |
|---|---|---|
| Phase 別表 | <!-- update-issue-tree:phase-plan:begin --> / <!-- update-issue-tree:phase-plan:end --> | マーカー間を毎回全置換(ルート直下の sub-issue 全件の表と、直下に open の子を持つ節点の詳細表) |
| 棚卸し履歴 | <!-- update-issue-tree:inventory:begin --> / <!-- update-issue-tree:inventory:end --> | 追記専用。既存行は保持し、今回の日付と 5 件数の 1 行を末尾へ足す(直前行と同一なら足さない) |
| granularity | <!-- granularity: Nh --> | 先頭に 1 行だけ維持する。値は GRANULARITY の確定値 |
- 既存本文から消してよいのは「phase-plan マーカー間の行」と「granularity マーカー行」だけ
## 概要・## 運用は管理しない。存在すれば逐語で保持し、存在しなくても追加しない (本文が空のときだけ骨格を生成する)- 表・件数・日付は実ツリーと検証済み引数からだけ生成する。5 件数の引数は既定値なしの必須で、 プレースホルダーが残っていれば API 呼び出し前に exit 1 で止まる
- issue タイトルは非信頼データ。
|・改行・<・-->を無害化して書き込むため、タイトルが管理マーカーを偽装できない - マーカーの不整合(片方のみ・重複・逆順・入れ子)と、閉じていないコードフェンスは編集せず exit 4 で止まる (閉じていないフェンスは以降を全部コード扱いにして管理ブロックを二重追記するため)
マーカーが無い既存本文の移行(現存するルートはすべてこの状態): 既存行は捨てない。
## Phase 別実装計画節があれば、見出し直後へマーカー付きの生成ブロックを挿入する。同節内の 見出し直後〜最初の H3 までの先頭部分と### Phaseで始まる H3 は旧生成物の候補として 削除せず、<details>(要約に退避日と「確認のうえ不要なら削除」を明記)へ逐語で移す。 それ以外の H3(### 実装ラン (...)等)は元の位置・内容のまま残す- 節が無ければ本文末尾へ
## Phase 別実装計画とマーカー付きブロックを追記する - 棚卸し履歴は
## 棚卸しで実施した整理(update-issue-tree 実行履歴)節を Phase 別実装計画の直前へ挿入する。 既存の手書きの棚卸し節には触れない - 退避が起きたら stdout 最終行の
migration=phase-plan-archivedを Step 9 の要確認事項へ転記する
書き込み前の確認と書き込み: 取得した本文と編集直前に再取得した本文が異なれば並行編集として
exit 5 で止まる。変更が無ければ result=unchanged で編集しない。--dry-run を付けると
マージ後の本文を stdout へ出し、書き込み系の gh を一切呼ばない(初回の移行前に推奨)。
件数変数の確定手順(各コードフェンスは独立したシェルで実行され得るため、Step 3〜7 のフェンス内の 変数は Step 8 へ引き継がれない。実績値は会話上で集計し、下記フェンスの先頭へ数値を直接書いた代入行として 追記してから実行する。推測・既定値・プレースホルダーのまま実行しない):
| 変数 | 確定する値 | 集計元 |
|---|---|---|
ROOT_NUMBER | ルート issue 番号 | 引数 |
GRANULARITY | 粒度(2h 等。Nh 形式) | 引数の確定値(既定 2h) |
REASSIGNED_COUNT | closed 親下の残置 issue 付け替えの成功件数 | Step 3 で reassign-sub-issue.sh が result=reassigned / posted-only を返した回数 |
ORPHAN_COUNT | 孤児 issue 再配置の成功件数 | Step 4 で同様に成功した回数 |
LABEL_SYNC_COUNT | phase ラベル同期の成功件数 | Step 6 で実際にラベルを付与・変更できた issue 数 |
NEW_PHASE_COUNT | 新設した Phase 親の件数 | Step 5 で作成と紐付けが成功した件数 |
SPLIT_COUNT | sub-issue 分解の成功件数 | Step 7 で分解(作成と紐付け)まで成功した issue 数 |
- 該当 Step を実行しなかった、または対象が無かった場合は
0を代入する(未設定のまま放置しない) - 失敗・見送り(非ゼロ終了、exit 10 / 11 を含む)は件数へ計上しない(Step 9 の集計規則と同じ)
- 代入例(値は実績に置き換える):
ROOT_NUMBER=254 GRANULARITY=2h REASSIGNED_COUNT=1 ORPHAN_COUNT=0 LABEL_SYNC_COUNT=3 NEW_PHASE_COUNT=0 SPLIT_COUNT=0をexport付きでフェンス先頭に置く
各コードフェンスは独立したシェルで実行され得るため、このフェンスは Step 3 / Step 4 と同形に スクリプトを 3 レイアウトから自己完結的に解決する。件数の変数が未設定なら起動前に停止する。
# Step 1〜7 の確定値。未設定ならここで停止する(プレースホルダーを本文へ流さない)
: "${ROOT_NUMBER:?}" "${GRANULARITY:?}" "${REASSIGNED_COUNT:?}" "${ORPHAN_COUNT:?}" \
"${LABEL_SYNC_COUNT:?}" "${NEW_PHASE_COUNT:?}" "${SPLIT_COUNT:?}"
UPDATE_ROOT_BODY=""
for CANDIDATE in \
"skills/update-issue-tree/scripts/update-root-body.sh" \
".agents/skills/update-issue-tree/scripts/update-root-body.sh" \
".claude/skills/update-issue-tree/scripts/update-root-body.sh"; do
# 存在確認は -f のみ(vendoring で実行ビットが落ちても bash 経由で起動できる)
if [[ -f "${CANDIDATE}" ]]; then
UPDATE_ROOT_BODY="${CANDIDATE}"
break
fi
done
if [[ -z "${UPDATE_ROOT_BODY}" ]]; then
echo "エラー: update-root-body.sh が見つからない(3 レイアウトいずれにも存在しない)" >&2
exit 1
fi
# echo を最終コマンドにしない。非ゼロ終了はそのままフェンスの終了ステータスにする
status=0
bash "${UPDATE_ROOT_BODY}" \
--root "${ROOT_NUMBER}" --granularity "${GRANULARITY}" \
--reassigned "${REASSIGNED_COUNT}" --orphans "${ORPHAN_COUNT}" \
--labels "${LABEL_SYNC_COUNT}" --new-phases "${NEW_PHASE_COUNT}" \
--split "${SPLIT_COUNT}" || status=$?
if (( status != 0 )); then
echo "exit=${status}" >&2
exit "${status}"
fi
終了コード(stdout 最終行は result=<updated|unchanged|dry-run> root=<n> migration=<none|phase-plan-archived|inserted> phases=<k>)
| exit | 意味 | 本文の変更 |
|---|---|---|
| 0 | 成功(updated のみ変更あり) | updated のみ |
| 1 | 引数エラー(件数のプレースホルダー残りを含む) | なし |
| 2 | 前提不備(gh / jq 不在・未認証・ルート取得失敗・ルートが PR) | なし |
| 3 | ツリー取得失敗・深さ超過(8 段)・表の行数不一致・65,536 文字超 | なし |
| 4 | 管理マーカーの不整合、または本文のコードフェンスが閉じていない | なし |
| 5 | 並行編集を検知 | なし |
| 6 | gh issue edit が失敗 | 不明(実状態を確認する) |
| 7 | 事後確認の不一致(再取得した本文全体が送信した本文と一致しない。末尾改行・CR の差は無視) | あり(編集済み) |
非ゼロ終了は握り潰さず Step 9 の要確認事項へ転記する。exit 6 / 7 は本文の実状態を確認する。 誤って更新した場合は GitHub の本文編集履歴から復元できる。
Step 9: 棚卸し結果を報告する
## update-issue-tree 完了レポート
### 対象ルート issue
- #N: タイトル
### 棚卸し実施内容
| 操作 | 件数 |
|------|------|
| closed 親下の残置 issue 付け替え | N 件 |
| 孤児 issue の再配置 | N 件 |
| phase ラベル同期 | N 件 |
| 新 Phase 親の新設 | N 件 |
| 粒度基準超 issue の sub-issue 分解 | N 件 |
### 現在の Phase 別サマリー
| Phase | 親 issue | open 件数 |
|-------|----------|----------|
| Phase 1 | #N | N 件 |
### ルート本文の更新結果
- result: updated / unchanged、migration: none / phase-plan-archived / inserted
### 要確認事項(自動配置できなかった issue)
- #N: タイトル — 確認理由
- 退避ブロック(`<details>`)の確認 / Step 8 の exit 5・6・7(該当時)
「closed 親下の残置 issue 付け替え」「孤児 issue の再配置」の件数は、Step 3 / Step 4 で
reassign-sub-issue.sh を呼んだ回数分の result= 行(reassigned / posted-only)から集計する。
すべての非ゼロ終了(exit 1〜8・10・11。今後コードが増えた場合も含む)は 1 件も件数へ含めず、
必ず「要確認事項」へ理由付きで記載する。とくに exit 8 は部分変更が残っているため、
報告を漏らすと壊れたツリーが放置される。exit 10(補償復旧成功)・exit 11(第三者が別親を設定済み)
も本来の付け替えは未達のため件数へは計上せず要確認事項へ記載する。exit 9・10・11 は Step 3 /
Step 4 のループ自体が fatal 扱いせず継続するため(終了コード表・上記ループを参照)、
これらは計画配列の残り全件を処理し終えたうえで Step 9 に到達する。Step 3 / Step 4 が stderr へ
出力する SKIPPED / NEEDS_REVIEW(および SKIPPED_ORPHANS / NEEDS_REVIEW_ORPHANS)の内容を
そのままこの節へ転記する。
検証
- ルート issue 本文の Phase 別表が更新されていることを確認する。あわせてマーカー外の節(概要・運用・手書き節)が更新前後で変わっていないこと、
YYYY-MM-DD・N 件等のプレースホルダーが本文に無いことを確認する(初回はupdate-root-body.sh ... --dry-runで出力を確認してから書き込む) - closed Phase 親の下に open issue が残置されていないことを確認する。ただし exit 10 (補償復旧成功)で要確認事項へ記録された issue は、承認範囲内の逆操作として旧親(closed) 配下へ意図的に復帰しているため、この確認の例外として扱う。 単独では「残置」に見えるので、 Step 9 の要確認事項の記録と突き合わせて exit 10 由来であることを確認する
- Step 3 / Step 4 で呼んだ
reassign-sub-issue.shの各回について、echo "exit=${status}"の値とresult=行を確認する。非ゼロ終了があれば Step 9 の要確認事項へ反映されているか確認する。 加えて、**exit 1〜8(9・10・11 を除く)**の fatal 系はコードブロック自体の終了ステータスが 非ゼロで返ること(echoを最後のコマンドにして握り潰していないこと)を確認する。 exit 9・10・11は fatal ではなくループ継続の契約であるため、コードブロックはそのまま 正常終了(終了ステータス 0)で最後のSKIPPED*/NEEDS_REVIEW*echo まで到達すること、 かつREASSIGN_PLAN/ORPHAN_PLANの残り全件が処理されていることを確認する
# 全 sub-issues の state を確認
gh api "repos/{owner}/{repo}/issues/${ROOT_NUMBER}/sub_issues" \
--jq '.[] | {number: .number, state: .state, title: .title}'
# Phase 親の sub-issues も確認
gh api "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues" \
--jq '.[] | {number: .number, state: .state}'
# phase ラベルの同期確認(各 Phase 親直下で確認)
gh api "repos/{owner}/{repo}/issues/${PHASE_NUMBER}/sub_issues" \
--jq '.[] | {number: .number, labels: [.labels[].name]}'
よくある失敗
| 問題 | 回避策 |
|---|---|
| 付け替えの DELETE が 404 になり、続く POST が 422 で失敗する | 削除のパスだけ単数形 sub_issue。複数形 sub_issues は 404 になり、旧親から外れないまま POST するため Sub issue may only have one parent で必ず失敗する(reassign-sub-issue.sh は DELETE 失敗時に POST へ進まないため、この連鎖失敗自体は起きない。手動で gh api を直接叩く場合の注意として記載を残す) |
| 管理ブロック内を手で編集したが次回の棚卸しで上書きされた | マーカー間は毎回再生成される。残したい内容はマーカーの外へ書く |
| 管理マーカーを片方だけ消して Step 8 が exit 4 になる | 両方を戻すか両方とも消す(マーカーを消すと次回は移行扱いになり、旧ブロックは <details> へ退避される) |
--granularity に 2 h・2・0h 等を渡して中断される | 正整数+h 形式(^[1-9][0-9]*h$。例: 2h・4h)で指定する |
注意事項
- 棚卸し前に変更内容をユーザーに提示して確認を取る(Step 2 参照)
- ページネーション: sub-issues が 100 件を超える場合は
per_page=100&page=Nでページングして全件取得する(Step 1 のツリー全体取得に適用。reassign-sub-issue.shは対象 issue のparent_issue_urlを直接参照するため、付け替え判定自体にはページネーションが不要) - シェルコマンドの変数は必ず
"${var}"でクォートする(コマンドインジェクション対策) gh issue createは--json非対応。issue URL を stdout に出力するため、| grep -oE '[0-9]+$'で末尾の番号を抽出する- sub_issues API(POST / DELETE)の
sub_issue_idは issue 番号ではなく database id(GitHub 仕様)。gh api "repos/{owner}/{repo}/issues/<number>" --jq '.id'で id を取得してから渡す。番号をそのまま渡すと誤った issue を操作する/404 になる(reassign-sub-issue.shはこれを内部で解決するため、Step 3/4 で手動取得する必要はない) - 孤児 issue の Phase が判断できない場合は推測せずにユーザーへ確認する
- sub_issues の DELETE API(付け替え時に旧親から外す操作)はパスが単数形
sub_issueである点に注意し、操作対象の issue 番号を必ず確認してから実行する - ツリー更新後は implement-issue-tree が post-order DFS で正しく消化できる構造になっているか確認する
- Step 3 / Step 4 の付け替え処理は
scripts/reassign-sub-issue.shを使う。SKILL.md 本文へ素のgh apiDELETE/POST を書き戻さない(状態変数の受け渡しがコードフェンス境界で壊れるクラスの欠陥に戻るため。詳細はscripts/reassign-sub-issue.sh冒頭コメントを参照) - Step 8 は
scripts/update-root-body.shを使う。gh issue edit --bodyでルート本文全体を書き戻さない(手書きの本文が消えるため。詳細はscripts/update-root-body.sh冒頭コメントを参照)。誤更新時は GitHub の本文編集履歴から復元できる
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/fandhe-ai/agent-cli-skills/update-issue-tree">View update-issue-tree on skillZs</a>