update-issue-tree
既存の GitHub Issue ツリーを棚卸し・更新するスキル。「ツリーを棚卸しして」「イシューツリーを更新して」「トラッキング issue を整理して」で使用。 ルートのトラッキング issue 番号を受け取り、sub_issues API でツリー全体を再帰取得 → closed 親下の残置 open issue 付け替え・孤児の再配置・新 Phase 親の新設・phase ラベル同期 → ルート issue 本文の Phase 別表・棚卸しセクションを再生成して更新する。 ツリー新規作成は create-issue-tree、実装消化は implement-issue-tree を参照
By fandhe-ai · 561 installs
npx skills add fandhe-ai/agent-cli-skills --skill update-issue-tree
Source repository · Upstream listing
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 行だけ保持する。
前提条件
gh CLI がインストールされ、認証済みであること( 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 で全件取得する。
以降の 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 で停止する。
対象が 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 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 11 reason=third party parent (既存の補償復旧経路と同じ扱い)。再取得の失敗・
不一致・不安定は exit 8 reason=recovery state unknown
孤児経路(DELETE なし)の POST 失敗 → 実状態を再取得し、孤児のまま安定確認できれば
exit 4 (無変更)。新親配下であることを安定確認できれば POST は偽陰性だった
とみなし result=posted only で exit 0 。この経路には DELETE が無く孤児化リスク自体が
無いため、補償の書き込み(POST)は発生しない(判定のみで完結する)。エラーメッセージ
ベースのレース検知( exit 7 。"only have one parent")は再取得より先に判定する。それ以外
(再取得の失敗・不一致・不安定・メッセージ非該当の第三者親)は exit 8
reason=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 の実行結果に依存せず単体