kasiopeiya

issue-deps

open Issue の依存を棚卸しし、ブロッカー節と段番号を直して、着手できる Issue の一覧を出す。「issue-deps」「issueの依存を棚卸しして」「着手順を振り直して」と指示されたとき。

kasiopeiya 0 Updated 4w ago
GitHub

Install

npx skillscat add kasiopeiya/claude-dev-template/issue-deps

Install via the SkillsCat registry.

SKILL.md

open Issue の依存を棚卸しし、## ブロッカー 節(先に終わっている必要がある Issue を書く欄)と段番号を直してください。段は着手順の層で、同じ段の Issue は互いに依存せず並行して着手できます。段番号はそれをタイトルの先頭に書いた数字です。

Issue の一覧を見ても、今日着手してよいものが分かりません。段番号は起票したときの見立てのままで、起点Issueを sub-issue(親を分解した子のIssue)に割ったあとに判明した順序が入らないからです。着手できない Issue に手を付ければ手戻りし、着手できる Issue を見落とせば手が止まります。

前提

  • 始めるタイミングは人間が握ります。 一度に多数の Issue のタイトルと本文を書き換えるため、いつ走らせるかは人間が決めます。
  • 起動後は確認なしで最後まで走ります。 誤った推定は次の棚卸しや着手時に人間が直せます。1件ずつ承認を取ると、Issue 数に比例して時間が伸びます。
  • リポジトリのファイルは変更しません。 触るのは GitHub Issue のタイトルと ## ブロッカー 節だけです。
  • 段番号の規約は Issueの階層ガイド が正典です。 以降の手順には計算に要る分だけを写しています(実行中にガイドを開かずに済ませるため)。規約と手順が食い違って見えたらガイドが勝ちます。
  • 親の付け替えと親の close は扱いません。 規約の改定を伴う不可逆な操作を、番号直しのついでに走らせないためです(#429 で扱います)。

Phase 1: 対象を決める

open Issue を、親・本文・ラベル・子の状態つきで一度に取ります。

gh repo view --json owner,name --jq '.owner.login + "/" + .name'
gh api graphql -f query='
{ repository(owner:"<owner>", name:"<repo>") {
    issues(first:100, states:OPEN) { nodes {
      number title body
      labels(first:20){nodes{name}}
      parent{number title}
      subIssues(first:100){nodes{state}}
    } }
} }'

ブロッカーから段を計算する対象から、次の2つを外します。

  • phase ラベルの常設Issue:分類であって着手する対象ではない
  • open な子を持つ Issue(state が OPEN の子が1件以上):親は子をまとめる入れ物で、着手単位ではない。段は子から範囲で付ける(Phase 4)

boy-scout ラベルの Issue は外しません。 正式フローを経ていなくても実際の実行単位なので、着手順が要ります。

subIssues.totalCount では判定しません。 close 済みの子も数えるので、子を全部閉じた親(残りを割る次の着手単位)が一覧から消えます。

残った Issue を、親を上に辿ってグループへ分けます。段番号はグループごとに1から振るので、この分け方が計算の単位になります。

  • フェーズIssueに行き着く:そのフェーズ
  • 親を持たない:「親なし」
  • 辿れない(中間の親が結果に無い):どこにも入れず、段を振らずに報告

parent は1階層しか返らないので、フェーズ → 起点Issue → スライスIssue のように深い場合は、取得結果の中で親をもう一度引いて辿ります。中間の親が close 済みだったり100件の外に落ちていると辿れません。そのときは推測でグループへ入れず、最後の報告に「所属不明」として並べます。

引数の解釈は次のとおりです。

  • なし:open な全グループ
  • フェーズ名:そのフェーズのグループだけ

フェーズ名は phase ラベルの Issue のタイトルと照合します。同じフェーズ名が複数あるときは、いちばん番号が大きいものを使います(階層ガイドの世代交代の規約)。

取得件数がちょうど100件なら上限で切れている可能性があるので、その旨を伝えてから処理します。対象が0件なら、その旨を伝えて終了します。決めた対象リストを表示してから Phase 2 へ進みます。

Phase 2: 依存を推定する

各 Issue の本文を読み、同じグループ内の他の Issue との依存を判定します。グループをまたぐ依存は数えません——フェーズ間の前後はフェーズ自体が表すからです。数えてよい依存は次の2つだけです。

依存 判定 例
成果物が無いと動かない A が作るファイル・関数・テーブル・Skill を、B が呼ぶ・編集する B の対応方針が、A が新設するファイルの節を参照している
決定が出るまで書き始められない A が決める方針(ADR・ポリシー・規約)に、B の中身が従う B が「A で決めた命名規約に合わせる」と書いている

定義を2つに絞るのは、推定結果を実行ごとにぶらさないためです。 「関係がありそう」まで広げると、同じ本文から毎回違う依存が出て、番号が実行ごとに動きます。

ファイル衝突(同じファイルを触る)は依存に含めません。 同じファイルを2件が触ることは並行作業の調整であって、着手順ではありません。含めると、順序が無いところに順序が生まれて段番号が実態より長く伸び、並行して着手できる Issue が待ちに見えます。

既存の ## ブロッカー 節の記述も、本文と突き合わせて毎回再判定します。 起票時に見えなかった依存は本文を読まないと見つからず、逆に、割った結果すでに解消した依存も残っています。

既存のブロッカー節に書かれた Issue 番号は、状態を1件ずつ確かめます。 Phase 1 で取るのは open だけなので、取得結果に無い番号が close 済みなのか100件の外なのかは、引かないと区別できません。(closed) の付与(Phase 3)と段1の判定(Phase 4)がどちらもこの区別に依存します。

gh issue view <番号> --json state --jq .state

グループの外を指す番号(別フェーズの Issue・phase ラベルIssue)は、消さずに残して報告します。 段の計算には入れません。書いた人が意図して置いた可能性があり、消すと理由ごと失われます。

Phase 3: ## ブロッカー 節を書き戻す

本文はこの節だけを差し替えます。 本文全体を組み直すと、推定で他の節が書き換わって失われます。

gh issue view <番号> --json body --jq .body > <スクラッチパッド>/issue-<番号>.md

書き出したファイルを Read し、Edit で ## ブロッカー 節の中身だけを差し替えてから反映します。節が無い Issue には新しく足します。置く位置は ## 実装フロー(使用するSkill) 節の直後です。その節も無ければ本文の末尾に足します。

gh issue edit <番号> --body-file <スクラッチパッド>/issue-<番号>.md

書く形は次のとおりです。Issue 番号のあとに、なぜそう判定したかを1文添えます。 根拠が無いと、次に開いた人は推定を信じてよいか判断できません。

## ブロッカー

- #123 — `/issue-deps` の SKILL.md を新設するIssue。本Issueの対応方針がその節を参照している

ブロッカーが無ければ「なし(すぐ着手できる)」と書きます。

close 済みの Issue をブロッカーに持つときは、その行を残したまま (closed) を付けます。 依存そのものは消えていないので、その事実を残します。段の計算では満たされた依存として扱います(Phase 4)。

Phase 4: 段番号を振り直す

グループごとに、## ブロッカー 節から段を計算します。

  • ブロッカーが無い、または全部 close 済み:1
  • open なブロッカーがある:ブロッカーの段の最大値+1

ブロッカーが無い Issue から順に確定させます。 段の定義がブロッカーの段を使うので、先を確定させないと計算できません。例:#A(ブロッカーなし)は段1、#B(#A を待つ)は段2、#C(#A と #B を待つ)は段3。

open な子を持つ親の段は、範囲で表します。 範囲は、その親の下にいる段を持つ open Issue(孫以下も含む)の段の最小値〜最大値です。同じグループの親をブロッカーに持つ Issue は、その範囲の最大値を「ブロッカーの段」として使います——親を待つとは、中の子を全部待つことだからです。自分の祖先をブロッカーに持つ Issue は、循環として扱います。

タイトルは、先頭の N. または N〜M. を剥がしてから付け直します。

  • open な子を持たない:<段>.
  • open な子を持つ親Issue:<最小>〜<最大>. 。最小と最大が同じなら <段>. 。段を持つ子孫が無ければ番号を外す
  • close 済み Issue:タイトルに触らない。他の Issue の段を計算するときは、満たされた依存として扱う
gh issue edit <番号> --title "<段または範囲>. <番号を除いたタイトル>"

gh を並列で叩かないでください。 1件ずつ順に処理します。同じ Issue へ複数の更新が同時に飛ぶと、あとから投げたほうが前の更新を消します。

Phase 5: 報告

グループごとに次の形で出します。人間はこれを見て、今日どれに着手するかを決めます。

着手できる(段1)

  • #123 …

待ち

Issue 段 待っているもの
#124 2 #123(成果物待ち)

親Issue(open な子を持つもの)

Issue タイトル 子の段
#120 全体構造設計 1〜3

グループに入れられなかった Issue(親を辿れなかったもの)と、グループの外を指すブロッカーが残っている Issue があれば、番号を並べて添えます。

末尾に、タイトルを書き換えた件数(親の範囲を付け直したものも数える)と ## ブロッカー 節を直した件数を1行で添えます。

エラーハンドリング

  • フェーズ名が phase ラベルのどのIssueにも一致しない:存在するフェーズ名を並べて終了する
  • ## ブロッカー 節が1つの本文に複数ある:最初の1つだけを差し替え、その事実を報告する
  • 依存が循環している:その Issue 群には段を振らず、循環の経路を報告する
  • gh issue edit が失敗する:その Issue の処理を中断し、エラーを表示して次の Issue へ進む
  • 同じ Issue に別セッションの更新が入っている:上書きせず中断し、その事実を報告する

使用方法

/issue-deps              # open な全グループ
/issue-deps 設計・実装   # そのフェーズのグループだけ

引数: $ARGUMENTS