kasiopeiya

issue-regroup

open な子が全部ブロッカー待ちで止まった親 Issue を、子を他の親へ移して close する。「issue-regroup」「親issueを閉じられるようにして」「子を付け替えて」と指示されたとき。

kasiopeiya 0 Updated 4w ago
GitHub

Install

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

Install via the SkillsCat registry.

SKILL.md

open な子が全部ブロッカー待ち(先に終わる必要がある Issue を待っている状態)で止まった親 Issue を、その子を待たせているブロッカーの親へ移し、元の親を close してください。

親の作業はほぼ終わっているのに、子1件が別の親の後でないと着手できないと分かって残ることがあります。その1件のせいで親が長期間 open のまま滞留すると、open な Issue の残り件数が実際の残作業を表さなくなり、一覧を見ても進み具合が分かりません。

前提

  • 始めるタイミングは人間が握ります。 親子の付け替えと親の close は元に戻しにくい操作なので、いつ走らせるかは人間が決めます。
  • 起動後は確認なしで最後まで走ります。 離脱条件(Phase 3)に当たった親・子は動かさずに報告するので、迷ったものが黙って動くことはありません。
  • リポジトリのファイルは変更しません。 触るのは GitHub Issue の本文・親子の紐付け・close と、Phase 2-2 で作る親 Issue だけです。
  • ## ブロッカー 節が埋まっていることを前提にします。 発火条件の判定はこの節だけを読みます。古いと感じたら、先に `/issue-deps` を走らせてください。
  • 段番号は振り直しません。 子が別のグループへ移ると段が変わりますが、番号の計算は /issue-deps の役目です。実行後に走らせるよう報告で促します。
  • 付け替えてよい条件は Issueの階層ガイド が正典です。 以降の手順には判定に要る分だけを写しています。規約と手順が食い違って見えたらガイドが勝ちます。

Phase 1: 対象の親を探す

はじめに引数で対象の範囲を決めます。

  • なし:open な全親
  • フェーズ名:親を上に辿った先がそのフェーズである親だけ

フェーズ名は phase ラベルの Issue のタイトルと照合します。同じフェーズ名が複数あるときは、いちばん番号が大きいものを使います(階層ガイドの世代交代の規約)。どの phase ラベルのタイトルにも一致しなければ、存在するフェーズ名を並べて終了します。

次に、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 id url
      labels(first:20){nodes{name}}
      parent{number title}
      subIssues(first:100){nodes{number title state}}
    } }
} }'

子を持つ open Issue を親の候補とし、次の順に絞ります。

  • phase ラベルの常設Issue:フェーズIssueは close しない(階層ガイド)
  • open な子が0件の親:付け替えずに閉じられる。/issue-check・/sweep の担当である
  • open な子に、ブロッカーが無いものがいる:着手できる子が残っているので滞留していない

残った親が発火条件を満たす親です。open な子が全部、open なブロッカーを1件以上持っています。

ブロッカーは各子の ## ブロッカー 節から読み、書かれた Issue 番号の状態を1件ずつ確かめます。Phase 1 で取るのは open だけなので、取得結果に無い番号が close 済みなのか100件の外なのかは、引かないと区別できません。close 済みのブロッカーは満たされた依存として扱い、待ちに数えません。

gh issue view <番号> --json state,url --jq '{state,url}'

取得件数がちょうど100件なら上限で切れている可能性があるので、その旨を伝えてから処理します。発火条件を満たす親が0件なら、何も変更せずにその旨を伝えて終了します。

Phase 2: 移動先を決める

子ごとに、その子を待たせている open なブロッカーの親を移動先にします。最後に完了するブロッカーへ寄せます——待ち終わる順に並べると、移した先でその子が最も早く着手できるからです。

段は着手順の層で、同じ段の Issue は互いに依存せず並行して着手できます。段番号はそれをタイトルの先頭に書いた数字です。段番号はグループ(親を上に辿って行き着くフェーズ)ごとに1から振るので、グループが違うブロッカー同士は段番号で比べられません。

  • 1つの親の中に揃っている:その親
  • 同じグループの複数の親に散っている:段番号が最大のブロッカーの親
  • グループが違う親に散っている:Issue 番号が最大のブロッカーの親
  • 段番号を持たないブロッカーしかない:Issue 番号が最大のブロッカーの親
  • 親を持たない Issue しかない:Phase 2-2 へ

比べられないときに Issue 番号を使うのは、起票が遅いものほど後の作業である公算が大きく、かつ実行するたびに同じ答えが出るからです。

Phase 2-2: 移動先の親が無いとき

ブロッカーが親を持たない Issue しかないときは、その子は行き先を失います。扱いは孤児の件数で決めます。

  • 同じ関心事の孤児が2件以上ある:その関心事の親Issueを新しく作り、まとめてぶら下げる
  • 1件だけ:移さない(離脱条件)。1件のために親を立てると子1つだけの入れ物になる

新しい親は umbrella(子への入口)として立てます。本文は「この親が何の塊か」を1文と、ぶら下げる子の一覧だけにします。タイトルに段番号は付けません(親に付ける子の段の範囲は、実行後の /issue-deps が計算します)。ラベルは boy-scout・ai-fixable・issue:needs-human-decision のどれも付けません(親は着手単位ではなく、/sweep・/issue-check が毎回拾って空回りします)。

新しい親は、移す子の元の親と同じ親(元の親の親)にぶら下げます。 そうしないとフェーズまで辿れなくなり、/issue-deps のグループ分けから外れます。

gh issue create --title "<関心事の名前>" --body "<何の塊かを1文>"

Phase 3: 離脱条件を確かめる

離脱条件は2つの粒度に分かれます。親単位に当たった親は何も変更せずに報告へ回します。子単位に当たった子は移さずに残し、移せる子だけを移して、その親は close しません。

粒度 離脱条件 なぜ動かさないか
親 移動先が元の親になる(兄弟待ちを含む) 動かす意味がない
親 依存が循環している(互いをブロッカーにする) 移動先が決まらない。循環の経路を報告し、人間に解かせる
子 子の本文に「トレース」節を書けない 要件へ戻る線が切れる。トレースを切るより親を open のまま残す方がよい
子 移動先の親が close 済み close 済みの親にぶら下げると、子が閉じた塊に隠れる
子 移動先の親の子が100件に達している addSubIssue が失敗する(親1件あたりの上限。階層ガイド)

1件でも移せない子が残る親は close しません。 親は open のまま残し、どの子がなぜ移せなかったかを報告します。

Phase 4: 子に「トレース」節を書き足す

付け替えの前に書きます。親子の紐付けは、その子が要件へ戻る唯一の線です(子の本文は親番号を持ちません——新規開発ガイドが「親Issueへの参照は本文に書かない」と定めています)。線を切る前に、子へ書き写します。

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

書き出したファイルを Read し、Edit で節を足します。本文はこの節の追加だけに限ります。 本文全体を組み直すと、推定で他の節が書き換わって失われます。

  • 「トレース」節が無い:節ごと足す

  • 既に「トレース」節がある:元の親の番号と移動理由の行だけを足す

  • 分かる(/to-issues 産など):そのテンプレートの節順(/to-issues 産なら「grill-me で確定した仕様」と「実装フロー」の間)

  • 分からない:本文の末尾

## トレース(要件定義書との対応)

- 要件: F-02(ポイント利用)/BR-07
- 元の親: #123(`/issue-regroup` で #456 へ移動。#456 の成果物を待つため)

要件の行に書く内容は、元の親の本文の「トレース」節から写します。元の親にその節が無ければ、子の本文と元の親の本文から機能ID・BR を探します。要件定義書に対応しない Issue(ハーネス・ポリシー・設計書の手直しなど)は「要件定義書に対応なし」と理由を1文添えて書きます。 要件由来かどうかが子と元の親の本文から判断できないときが「書けない」で、その子は移しません(Phase 3 の離脱条件)。

反映する直前に本文を引き直し、書き出した時点と変わっていないことを確かめます。 変わっていれば別セッションが編集しているので、上書きせずその子の処理を中断します。

gh issue view <子の番号> --json body --jq .body | diff - <スクラッチパッド>/issue-<番号>.orig.md
gh issue edit <子の番号> --body-file <スクラッチパッド>/issue-<番号>.md

Phase 5: 付け替えて、元の親を close する

addSubIssue の replaceParent: true で移します。1回の呼び出しで元の親から外して新しい親に付くので、removeSubIssue と2回に分けません。

gh api graphql -f query='mutation($parent:ID!,$childUrl:String!){addSubIssue(input:{issueId:$parent,subIssueUrl:$childUrl,replaceParent:true}){subIssue{number}}}' \
  -F parent="$(gh issue view <移動先の親の番号> --json id --jq .id)" -F childUrl="<子のURL>"

その親の子を全部移し終えたら、open な子が1件も残っていないことを引いて確かめてから close します。

gh api graphql -f query='{repository(owner:"<owner>",name:"<repo>"){issue(number:<番号>){subIssues(first:100){nodes{number state}}}}}'
gh issue close <元の親の番号> --comment "open な子が全部ブロッカー待ちだったため、#<移動先> へ移して close した。移した子: #<番号>, #<番号>"

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

Phase 6: 報告

移した子

子 元の親 新しい親 移した理由
#124 #123 #456 #457 の成果物を待つ

close した親

親 タイトル 移した子の数
#123 … 2

動かさなかった親・子

対象 粒度 離脱条件
#130 親 移動先が元の親になる
#131 子 トレース節を書けない

新しく作った親があれば、番号とぶら下げた子を添えます。

末尾に、段番号が実態とずれたことと、/issue-deps を走らせると直ることを1行で書きます(子が別のグループへ移ったため)。

エラーハンドリング

  • 子の本文に ## ブロッカー 節が無い:ブロッカー無しとは見なさず、その親を動かさずに報告する(判定できない)
  • ## ブロッカー 節が1つの本文に複数ある:最初の1つだけを読み、その事実を報告する
  • addSubIssue が失敗する:その子の処理を中断し、親を close せずに報告する(トレース節は残る)
  • gh issue edit が失敗する:その子は移さず、親を close せずに報告する
  • 反映直前の引き直しで本文が変わっている:上書きせず、その子の処理を中断して報告する

使用方法

/issue-regroup              # open な全親
/issue-regroup 設計・実装   # そのフェーズの親だけ

引数: $ARGUMENTS