link-root-issue

起票した Issue をルート Issue(Issue 階層の最上位の常設 Issue)の下に紐づける。起票前にルートを決める段と、起票後に紐づける段を持つ。Issue を起票する Skill が、起票の前後に Read して手順に従う。「link-root-issue」「ルート Issue に紐づけて」と指示されたとき。

SKILL.md

GitHub

Link Root Issue

分解元の親が無い Issue を、ルート Issue の sub-issue にする。ルート Issue の作成と、上限に達したときの世代交代も、ここが持つ。

手順は2段に分かれる。ルートを決める段は起票の前に、紐づける段は起票の後に行う。段番号(タイトルの先頭に付ける着手順の数字)は、決めたルートの中で1から数えるので、ルートは起票時のタイトルを書く前に要る。

呼び出し元(/quick-issue など)は、Skill ツールで呼ばずに、この SKILL.md を Read して手順に従う。Skill ツールを持たない呼び出し元(fork した agent、allowed-tools に Skill が無い Skill)があるためである。Skill として直接呼ぶ(/link-root-issue 1129)のは起票済みの Issue に対してなので、2つの段を続けて行う。

前提

  • 方針は issue-policy の「ルート Issue」が定める。始める前にこのポリシーを Read する。
  • このプロジェクトが使うルートと振り分け条件は、docs/project-context/issue-destinations.md(起票先ファイル)が持つ。これも Read する。
  • 起票先ファイルにあるルートとは、ファイルの表の「ルート」列の名前をいう。ファイルに「issue-policy の標準のルートを使う」とあるなら、ポリシーの標準のルートの名前をいう。
  • 分解元の親が無いのに起票先ファイルも無いときは、紐づけずにその旨を報告して終える。ファイルが SSOT(単一の正典)なので、ファイルにあるルート以外の名前では作らない。分解元の親があるときは、ファイルを使わない(下の1-1)

引数

<Issue番号> [ルート名]

  • Issue 番号:紐づける Issue。起票前にルートを決める段は、まだ Issue が無いので番号を取らない(呼び出し元が Read して行う)
  • ルート名:起点 Issue(新規開発ガイドが要件定義書から作る Issue)のように、親が種別で決まるときに渡す。新規開発ガイドの表の「親にするフェーズ」をそのまま渡す。省くと、起票先ファイルの条件で決める

処理フロー

1. ルートを決める(起票前)

次の優先順位で、最初に決まったものを使う。

  1. 分解元の親がある:その親を親にする。親への紐付けは呼び出し元が行い、この Skill の「2. 紐づける(起票後)」は行わない。ルートは決めず、段番号のグループは、その親を上に辿って行き着いたルートにする(起票先ファイルが無くてもよい)
  2. ルート名を渡された:そのルート。起票先ファイルにあるルートに無い名前なら、紐づけずに報告して終える(ファイルが SSOT なので、作らない)
  3. 起票先ファイルの条件:上の行から当て、最初に当たった行のルート。既定の行は最後にある。起票前は、これから付けるラベルと触る予定のパスに当てる。起票済みの Issue(直接呼ばれたとき)は、付いているラベルに当てる。「issue-policy の標準のルートを使う」とあるなら、ポリシーの標準のルートの「主に触るもの」を上から当てる

決めたルート名を、段番号のグループとして呼び出し元に返す。ルートが決まらず終えたとき(分解元の親が無く起票先ファイルが無い、ルート名がファイルに無い)は、「親なし」のグループを返す。

2. 紐づける(起票後)

すでに親がある Issue には何もしない。 分解元の親を優先する。

2-1. ルートを探す

ルートは、open で phase ラベルが付き、タイトルが「ルート名」か、2-4 で作る後継のタイトルの Issue のうち、番号が最大のものを使う。世代交代した後継も見つけるためである。完全一致だけで探すと、後継を見つけられず、呼ぶたびに作り直す。

gh issue list --label phase --state open --json number,title

2-2. ルートが無ければ作る

上の gh issue list が成功し、決めたルート名に当たる Issue が無いときは、起票先ファイルにあるルートのうち、無いものをすべて作ってから紐づける。 要るものだけ作ると、次の起票で残りのルートが無いまま親を選ぶことになる。コマンドが失敗したときは作らず、紐付けの失敗と同じく扱う(重複して作らないため)。

ルートは常設の分類なので、/quick-issue の本文テンプレートを使わず、3択も付けない(ポリシーの「3択の例外」)。本文には、起票先ファイルの表にある「ルートの本文」を書く。標準のルートなら、ポリシーの「標準のルート」にある、そのフェーズにぶら下げるものを書く。

gh issue create --title "<ルート名>" --label phase --body "<ルートの本文>"

gh が could not add label: 'phase' not found で失敗したら、gh label create phase --description "<scripts/setup-labels.sh の説明>" で作ってからやり直す。ほかの文言で失敗したときは、作らず紐付けの失敗と同じく扱う。

2-3. 紐づける

GraphQL の形は worktree で動くセッションのガードに拒否されるので、REST で紐づける。

gh api repos/<owner>/<repo>/issues/<子の番号> --jq .id
gh api --method POST repos/<owner>/<repo>/issues/<ルートの番号>/sub_issues -F sub_issue_id=<子のデータベース ID>

紐付けに失敗しても、起票そのものは成功している。 失敗したら親が付かなかったことを伝えて続行し、起票をやり直さない(同じ Issue を二重に立てることになる)。ただし、ルートの sub-issue が上限に達しているときは、2-4 の世代交代を行ってから紐づけ直す。

2-4. 上限に達したら世代交代する

GitHub の sub-issue は親1件あたり100件が上限で、超えると紐付けが失敗する(sub_issues_summary.total は close 済みの子も数える)。close した子は枠を空けない。 GitHub が上限到達時に案内するのは「sub-issue の紐付けを外す」か「新しい親を作る」のどちらかで、close は含まれない。

紐付けに失敗したら、エラー文ではなく件数で上限かどうかを決める。ルートの sub-issue の数を引き、100以上のときだけ世代交代する。100未満なら、2-3 のとおり、親が付かなかったと報告して続ける(権限不足や ID の誤りで後継を作ると、以後の紐付けがすべて後継へ流れるため)。

gh api repos/<owner>/<repo>/issues/<ルートの番号> --jq .sub_issues_summary.total

上限に当たったら、「ルート名 (N)」の後継を作り、以後の紐付け先をそちらに切り替える。N は、いま使っているルートのタイトルの番号に1を足した数である(番号の無いタイトルは1とみなすので、最初の後継は (2) になる)。旧ルートは close せず、履歴として残す。

gh issue create --title "<ルート名> (<N>)" --label phase \
  --body "<ルートの本文>。#<前のルート Issue 番号> の後継。"

完了の報告

決めたルート名と、紐づけたルートの番号(失敗したなら、その旨)を呼び出し元に返す。

使用方法

/link-root-issue 1129            # 起票先ファイルの条件でルートを決めて紐づける
/link-root-issue 1130 要件定義   # ルート名を指定して紐づける(起点 Issue など)