mcpField
I
issue-tracker

issue-tracker

github.com/clhbid開発0488 回インストール3 日前に更新
リポジトリ

issue-tracker について

issue-tracker は、CLHbid リポジトリ向けの GitHub issue とプロジェクトボードの運用ガイドです。issue の種類や状態、参照、トリアージ、デリバリーの手順を統一し、gh コマンドラインで操作します。

主な機能

  1. 01issue の分類とライフサイクル用語を定義
  2. 02完全な issue 参照形式を統一
  3. 03プロジェクト状態とトリアージ役割を対応付け
  4. 04gh コマンドラインでの issue 操作を説明
  5. 05ボード検索とデリバリー構成を解説

活用シーン

  1. 01リポジトリの issue の作成や更新
  2. 02トリアージ状態に沿った issue の振り分け
  3. 03CLHbid リポジトリ横断の作業追跡
  4. 04操作前の issue 参照の解決

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/clhbid/agent-context --skill issue-tracker

インストール後、Claude Code で/issue-trackerを入力して読み込まれたか確認します。

同梱ファイルboard.graphqlsnapshot.graphql

詳細

イシュートラッカー:GitHub

このリポジトリのイシューと仕様は、GitHub のイシューで管理します。作業状況は、すべての clhbid リポジトリにまたがる組織プロジェクト CLHbid Delivery で管理します。すべての操作には gh CLI を使用してください。

用語

スキルが計画し、報告する際に使う用語です。以下のルールはすべて、これらの用語に基づいています。

サイクル(Cycle):作業をコミットする、2 週間程度の計画期間です。イシューをサイクルにコミットすることは、そのサイクル内に完了する約束を意味します。 避ける用語:スプリント、マイルストーン。

カテゴリ(Category):イシューの種類を表し、イシューの種類として設定します。Bug、Enhancement、Epic のいずれか 1 つを、各イシューに設定します。 避ける用語:ラベル、種別。

エピック(Epic):Epic のイシューの種類が設定されたトップレベルのイシューです。1 つのサイクルで完了するには大きすぎるため、子イシューを通じて実施します。エピックをサイクルにコミットすることはありません。各子イシューは 1 つのサイクルで完了できる規模であり、個別にコミットします。 避ける用語:イニシアチブ、プログラム、トラッカーイシュー、親(エピックはすべて親ですが、親のうちエピックなのは一部です)。

コミット対象ユニット(Committed unit):Cycle が設定されているイシューです。トップレベルのイシュー、またはエピックの子イシューを指します。その子孫はサイクルを継承し、直接設定することはありません。 避ける用語:計画済みイシュー、スケジュール済みイシュー。

未完了(Slipped):サイクルにコミットされ、そのサイクルが終了した時点でも未完了の状態です。必ずコミット未達を意味し、「想定どおり進行中」を意味することはありません。 避ける用語:進行中、持ち越し。

進捗(Progress):エピックの完了した子イシュー数を、子イシューの総数で割ったものです。 避ける用語:完了率、ベロシティ、バーンダウン。

業務作業(Business work):業務で計画し、報告する作業です。トップレベルのイシュー、またはエピックの子イシューを指します。 避ける用語:チケット、ストーリー。

規約

  • イシューを作成する:gh issue create --title "..." --body "..."。複数行の本文にはヒアドキュメントを使用します。この方法ではイシューフォームも種類も適用されないため、 組織フォームからイシューを作成する の手順に従ってください。

  • 読み取り、一覧表示、コメント:gh issue view <number> --comments、gh issue list、 gh issue comment <number> --body "..."。--jq には --json が必要です。そのため、読み取り結果をフィルタリングする場合は --comments を外し、フィールドを指定します:gh issue view <number> --json number,title,labels --jq ...

  • イシューをサブイシューにする:作成後に gh issue edit <child> --parent <parent-url> を実行します。 組織フォームからイシューを作成する にある REST API での作成では、親を指定できません。URL 形式を使うと、エピックの子イシューによくあるように、別のリポジトリにある親も指定できます。

  • イシューを参照する:完全な参照を使用してください。参照 を参照してください。

  • 状態を設定する:ラベルではなく、プロジェクトのフィールドを使います。ステータス を参照してください。

  • クローズする:イシューをクローズする理由は completed、not planned、duplicate のいずれかです。理由は記録として残るため、該当するものを選び、クローズコメントで理由を説明してください。

    # Completed — the work is done
    gh issue close <number> --reason completed --comment "Shipped in #<pr>."
    
    # Not planned — we are not delivering it, abandoned and deferred alike
    gh issue close <number> --reason "not planned" --comment "Deferred to <date>; <owner> holds it."
    
    # Duplicate — another issue carries the work
    gh issue close <number> --duplicate-of <other-number> --comment "Tracked under #<other-number>."
    
    • --reason を省略した場合、デフォルトは completed です。
    • not planned は 2 語です。not-planned は受け付けられません。延期または中止の記録はこれだけで完結するため、ほかに何も必要ありません。
    • --duplicate-of を指定すると、理由が duplicate に設定され、2 つのイシューがリンクされます。これにより、残すイシューのスレッドが履歴になります。オープンのまま残すイシューの番号を指定してください。
  • クローズ理由を修正する:gh issue edit では理由を設定できず、すでにクローズされたイシューに対して gh issue close を実行しても何も起こりません。completed と not planned の場合は、PATCH します: gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f state=closed -f state_reason=not_planned 後から重複を記録する場合は、再オープンしてから再度クローズします: gh issue reopen <number> && gh issue close <number> --duplicate-of <other-number>。

参照

すべての参照は、文章中でも表中でも <org>/<repo>#<number> の形式で完全に記述してください。 GitHub はこれをホバーカード付きのリンクとして表示し、同じリポジトリ内の場合は #<number> に短縮するため、読む側の負担にはなりません。単独の #<number> は、それが記載されたテキストをホストするリポジトリを参照します。組織のディスカッションでは、意図したリポジトリではなく clhbid/clhbid.com が参照され、誤った Issue に気づかないままリンクされます。

短い参照は読み取り、完全な形式で記述してください。 単独の #<number> や、以下のエイリアスを大文字と小文字を区別せずに使う人がいます。対応する Issue を解決してから作業し、どの Issue に解決したかを伝えてください。短い参照を含むドキュメントやメモを編集するときは、完全な形式への置き換えを提案してください。エイリアスが不明な場合や、2 つの Issue を指す可能性がある場合は確認し、表にないリポジトリには短いエイリアスを提案してください。

エイリアスリポジトリ
LAclhbid/CLHbid-LiveAuction
comclhbid/clhbid.com
ACclhbid/agent-context
infraclhbid/infrastructure
TZ、hotpatchclhbid/canadian-time-zone-hotpatch
DFclhbid/devcontainer-features
gh、.githubclhbid/.github
scriptsclhbid/clhbid-scripts
E2Iclhbid/email-to-issue
SFETclhbid/salesforce-email-templates
templateclhbid/repository-template

Issue とプルリクエストでは番号が共通のため、番号を確認するには gh pr view <n> を実行し、見つからなければ gh issue view <n> を実行してください。

ステータス

CLHbid Delivery プロジェクトの Status は、イシューの状態を示す唯一の信頼できる情報源です。

ステータス意味
Backlog受信箱です。まだ振り分けられていないすべての項目が含まれます。新しいイシューはここに追加されます
Waiting on input報告者またはビジネスに確認を求めており、回答があるまで先に進められません
Ready for Agent仕様が完全に定まり、1つのプルリクエストに分割され、エージェント向けのブリーフが付いています
Ready for Human人による対応が必要です。判断、外部システム、手動での検証、またはレビュー対象のプルリクエストなどです
In progress担当者が決まり、作業中です
Done理由を問わずクローズされています(completed、not planned、duplicate)

それ以外はネイティブのままにし、ボード上に重複させません。担当者(誰が担当しているか)、リンクされたプルリクエスト(何がレビュー中か)、サブイシュー(分解)、イシューの依存関係(ブロックしているもの)、クローズ済み(完了)。

トリアージによって Backlog を空にします。 すべてのイシューはここに届き、Ready for Agent、Ready for Human、Waiting on input へ振り分けられるか、クローズされて離れます。検討したうえで意図的に延期する作業は Backlog に戻し、次回に再検討します。そのため、Backlog は「すでに対応済み」を意味せず、受信箱がいっぱいなのは通常の状態であり、トリアージの未処理分が溜まっている状態ではありません。

Waiting on input が空なのは健全な状態であり、調査が必要な問題ではありません。ビジネス側でブロックされているものがないことを意味します。

Ready for Agent を優先してください。 このステータスには、行の両方の条件、つまり仕様が完全に定まっていることと、すでに分割されていることが必要です。何かを決めるまで仕様を定められない作業は Waiting on input です。これはスケジュールする作業ではなく、回答すべき質問です。Ready for Human は、エージェントでは完了できない作業に使います。エージェント向けブリーフに含める内容については、afk-loop § エージェント向けブリーフを参照してください。

ビジネスへの確認はメールで行います。 ビジネス上の判断が必要になったら、サイクルレビューまで保留せず、その都度メールで確認します。サイクルレビューは、実際に話し合いが必要なことを扱う場です。メールには、何が起きているかを平易な言葉で説明した内容、必要な判断、各選択肢とその結果、推奨案とその理由、イシューへのリンクを記載します。

担当の割り当ては assignee の書き込みです。 gh issue edit <n> --add-assignee @me は、2つのエージェントが同じイシューを担当するのを防ぐ、最初のアトミックな書き込みです。その後に Status を In progress に設定します。

ボードにないイシューには状態がなく、以下のすべてのクエリから見えません。自動追加ワークフローは、新しく作成されたイシューをボードに追加します。作成した後は、ワークフローに少し時間を与えてから追加されたことを確認し、追加されていないものがあれば追加してください。

イシューを1つ追加すると、そのツリー全体が追加されます。 親を追加すると、リポジトリの境界を越え、スコープに含めていなかったリポジトリも含めて、すべての子孫が追加されます。そのため、計画がコミット済みの単位に設定されていても、ボードが浅くなるわけではありません。大半は子孫であり、Cycle が未設定のまま追加されます。そのため、Cycle でグループ化したビューには大きな No Cycle の区分ができます。これは、計画済みのコミット済み祖先を持つ子孫と、Cycle が設定されることのないエピックです。

ステータスの設定

Copilot セッションでは、ボードコマンドと、ボードのクエリのコマンドを、GH_TOKEN="$COPILOT_PROJECT_TOKEN" を指定して実行します。セッション自体のトークンでアクセスできるのは 1 つのリポジトリですが、このトークンはボードへの書き込みと issue の読み取りに使います。このトークンは、clhbid/clhbid.com の docs/environment-variables.md に従って、所有者がローテーションします。

gh project item-edit には 3 つの ID が必要です。セッションごとに 1 回、調べてください。

# project id, Status field id, and the option id for each status
gh api graphql -f query='
  query { organization(login: "clhbid") { projectV2(number: 4) {
    id field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } } } } }'

# the item id for an issue (an issue may sit on several projects — take project 4)
gh api graphql -f query='
  query($owner: String!, $repo: String!, $num: Int!) {
    repository(owner: $owner, name: $repo) { issue(number: $num) {
      projectItems(first: 10) { nodes { id project { number } } } } } }' \
  -f owner="$(gh repo view --json owner --jq .owner.login)" \
  -f repo="$(gh repo view --json name --jq .name)" \
  -F num=<number> \
  --jq '.data.repository.issue.projectItems.nodes[] | select(.project.number == 4) | .id'

リポジトリは必ずチェックアウトから取得し、値を直接指定しないでください。 ボードはすべての clhbid リポジトリを対象とし、issue 番号はリポジトリごとに割り当てられるため、repo= を固定すると、別のリポジトリにある誤った issue が解決されます。通常は NOT_FOUND になりますが、両方のリポジトリに同じ番号が存在すると、誤った issue の実際の item id が返され、item-edit はそれを受け付けてしまいます。

続いて、次を実行します。

gh project item-edit --project-id <project-id> --id <item-id> \
  --field-id <status-field-id> --single-select-option-id <option-id>

その後、値を読み返してください。誤ったオプション ID を指定しても、警告なしで受け付けられます。

トリアージの役割

skills では、2 つのカテゴリと 5 つの状態からなる、7 つの標準的なトリアージの役割を使います。ここでは、いずれもラベルではありません。カテゴリの役割はissue タイプ(エピックを参照)、状態の役割は**Status の値**です(それぞれの意味はステータスを参照)。

mattpocock/skills での役割ここでの値
bugタイプ Bug
enhancementタイプ Enhancement
needs-triageBacklog
needs-infoWaiting on input
ready-for-agentReady for Agent
ready-for-humanReady for Human
wontfix--reason "not planned" を指定してクローズ

skill に「カテゴリを適用する」とある場合は、タイプを設定します。「AFK 対応可能のトリアージラベルを適用する」とある場合は、この表の値を Status に設定します。 In progress と Done に相当する値は、この用語体系にはありません。

ビジネスとデリバリー

作業の分解はデリバリー上の関心事です。 作業はサブ issue に分割され、1 つずつプルリクエストで追跡、切り分け、レビューできるようにします。詳しくはReady for Agent にする前の作業の分解を参照してください。ビジネスでは、この粒度で作業を追跡しません。トップレベルの issue がビジネス作業の単位であり、その子 issue は作業の進め方を表します。例外はエピックです。エピックの子 issue は、それぞれが独立してサイクルにコミットされるため、各自がビジネス作業として扱われます。

ボードには、2 つの対象者に向けたビューがあります。📋 Delivery board はエピック以外のすべて、💼 Business は no:parent-issue -type:Epic、🗺️ Epics は進捗付きのエピックです。

ボードのクエリ

issue 検索ではプロジェクトフィールドを読み取れません。status:"In progress" は検索条件ではないため、何も一致しません。そのため、状態に関するクエリはすべて、board.graphql を使ってボードに対して実行し、用途に応じて --jq で絞り込みます。

まず board.graphql を見つけてください。 このファイルと snapshot.graphql は同じ場所にありますが、その場所は skill のインストール方法によって異なります。セッションごとに 1 回解決し、以降はそのパスを使ってください。

for d in ~/.claude/skills/issue-tracker \
         ~/.agents/skills/issue-tracker \
         .claude/skills/issue-tracker; do
  [ -f "$d/board.graphql" ] && BOARD_QUERY="$d/board.graphql" \
    && SNAPSHOT_QUERY="$d/snapshot.graphql" && break
done

どの場所にも存在しない場合は、推測せず、そのことを伝えてください。以下のレシピはすべて、このファイルを必要とします。

# Agent frontier — ready, leaf, unblocked, unclaimed
gh api graphql --paginate -F query=@"$BOARD_QUERY" --jq '
  .data.organization.projectV2.items.nodes[]
  | select(.content.state == "OPEN" and .status.name == "Ready for Agent"
           and .content.assignees.totalCount == 0
           and .content.subIssues.totalCount == 0
           and .content.issueDependenciesSummary.blockedBy == 0)
  | "\(.content.repository.name)#\(.content.number)  \(.content.title)"'

# Needs triage — the inbox
... | select(.content.state == "OPEN" and .status.name == "Backlog")

# Needs business input
... | select(.content.state == "OPEN" and .status.name == "Waiting on input")

# In flight
... | select(.content.state == "OPEN" and .content.assignees.totalCount > 0)

# Security — every open issue labelled security
... | select(.content.state == "OPEN" and any(.content.labels.nodes[].name; . == "security"))

# Business filter — top-level issues and epic children, never an epic itself. Goes at the head of
# the jq program, before `.data`, in any recipe that wants its business view
def business: .content.issueType.name != "Epic"
              and (.content.parent == null or .content.parent.issueType.name == "Epic");

# Planning view — open business work
... | select(.content.state == "OPEN" and business)

# Epics — every open epic with its progress
... | select(.content.state == "OPEN" and .content.issueType.name == "Epic")
    | "\(.content.repository.name)#\(.content.number)  \(.content.subIssuesSummary.completed) of \(.content.subIssuesSummary.total)  \(.content.title)"

# Epics ready to close — open, with every child closed. A person closes them
... | select(.content.state == "OPEN" and .content.issueType.name == "Epic"
             and .content.subIssuesSummary.total > 0
             and .content.subIssuesSummary.completed == .content.subIssuesSummary.total)

# Epic candidates — committed parents that are not epics. Read each one; the recipe only narrows
... | select(.content.state == "OPEN" and .content.issueType.name != "Epic"
             and .content.parent == null and .content.subIssuesSummary.total > 0
             and .cycle != null)
    | "\(.content.repository.name)#\(.content.number)  \(.content.subIssuesSummary.completed) of \(.content.subIssuesSummary.total)  \(.content.title)"

# Status/state mismatch — the Item closed workflow missing one. Healthy result is empty
... | select((.content.state == "CLOSED" and .status.name != "Done")
          or (.content.state == "OPEN"   and .status.name == "Done"))

# Worked off-cycle — work that never got credited to the cycle it happened in
... | select((.content.state == "CLOSED"
              and .content.closedAt >= env.CYCLE_START and .content.closedAt < env.CYCLE_END
              and (.cycle.title // "") != env.CYCLE_TITLE)
          or (.content.state == "OPEN" and .status.name == "In progress" and .cycle == null
              and .content.updatedAt >= env.CYCLE_START))

# ...of which dormant — swap that last clause for uncycled In progress work nobody touched
              and .content.updatedAt < env.CYCLE_START))

# Stale closed items — closed but never archived. Healthy result is empty
... | select(.content.state == "CLOSED"
             and (.content.closedAt | fromdateiso8601) < (now - 4838400))

# Stale issues — business work untouched for eight weeks
... | select(.content.state == "OPEN" and business
             and (.status.name | IN("Backlog", "Ready for Agent", "Ready for Human"))
             and (.content.updatedAt | fromdateiso8601) < (now - 4838400))

# ...of which newly stale — add this clause to keep the ones not yet stale when the cycle began
             and (.content.updatedAt | fromdateiso8601)
                 >= ((env.CYCLE_START | strptime("%Y-%m-%d") | mktime) - 4838400)

... は上記の完全なコマンドを表します。--paginate はページごとに --jq を適用するため、絞り込みと一覧表示は記載どおりに動作します。件数の集計にはパイプ(| wc -l)が必要です。

アーカイブ済みアイテムはここでは表示されません。 ProjectV2.items は archivedStates: [NOT_ARCHIVED] をデフォルトで使用するため、すべてのレシピがアクティブなボードのみを読み取ります。そのため、古くなったクローズ済みアイテムを処理するレシピは再実行できます。直前にアーカイブしたアイテムは表示されないからです。また、snapshot.graphql では archivedStates: [ARCHIVED, NOT_ARCHIVED] を明示的に渡します。 8 週間は 4838400 秒であり、stale の定義です。アイテムが stale かどうかはボード上で決まり、誰かの判断によるものではありません。また、stale かどうかは業務上の作業を基準に測定します。サイクルのレシピでは環境変数から CYCLE_TITLE、CYCLE_START、CYCLE_END を読み取ります。イテレーションの title、startDate、startDate + duration を設定してください。CYCLE_END は排他的です。イテレーションは、そのイテレーションを締めくくるミーティングの前日に終了するため、「14 Aug → 28 Aug」というタイトルのサイクルでは、CYCLE_START=2026-08-14、CYCLE_END=2026-08-28 となります。

新たに stale になったかどうかは導出するものであり、保存はしません。 1 つの updatedAt を 2 つのしきい値と比較します。今日を基準に stale かどうか、そして CYCLE_START 時点ではまだ stale ではなかったかどうかです。サイクル終了時にスナップショットを取るベースラインはありません。代わりに CYCLE_END を基準にすると、今日から今日までの間に stale になったアイテムを尋ねることになり、何も返されません。

プルリクエストはボード上のアイテムではありません。そのため、以下のレシピではリポジトリごとに gh pr list を実行し、タイトルの NNNN: プレフィックスまたはブランチの NNNN- プレフィックスからイシューを特定します(コミット規約を参照)。

# Merged pull request, open issue — a closing reference that never fired, or none. Healthy result is empty
for repo in $(gh repo list clhbid --no-archived --json name,hasIssuesEnabled --jq '.[] | select(.hasIssuesEnabled) | .name'); do
  open=$(gh issue list --repo "clhbid/$repo" --state open --limit 500 --json number --jq '.[].number')
  gh pr list --repo "clhbid/$repo" --state merged --limit 200 --json number,title,headRefName,closingIssuesReferences \
    --jq '.[] | ((.title | capture("^#?(?<n>[0-9]+)[: ]")?) // (.headRefName | capture("(^|/)(?<n>[0-9]+)-")?) // empty) as $m
          | "\($m.n)\t\(.number)\t\(.closingIssuesReferences | length)\t\(.title)"' \
  | while IFS=$'\t' read -r n pr refs title; do
      grep -qx "$n" <<<"$open" && echo "$repo#$n open — merged in $repo#$pr, $refs closing reference(s)  $title"
    done
done

クローズ参照が効力を持つのは、デフォルトブランチへのマージ時のみです。 クローズ参照があり、イシューがオープンの行は、別の場所にマージされたプルリクエストです。GitHub がターゲットを変更する前に親へマージされた積み重ねられたスライスや、scale-up などのブランチにマージされたものが該当します。クローズ参照がない行は、Closes 行がなかったか、意図的に Refs/Part of を使ったものです。どちらの場合も、プルリクエストが一部対応で、イシューが引き続き有効な場合を除き、規約に従ってプルリクエストの名前を記載し、手動でイシューを completed にします。

これらのレシピが標準です。ビューの内容を返す API はないため、ビューを直接クエリすることはできず、ミラーリングのみ可能です。

#ビューフィルター目的
1📋 デリバリーボード-type:Epicエピック以外のすべて。Status でグループ化
2🎯 今サイクルcycle:@current実行中のサイクル
3⏭️ 次サイクルcycle:@next次に予定されている作業
4💼 業務no:parent-issue -type:Epicトップレベルの作業。完了済みと未完了の両方
5🤖 フロンティアstatus:"Ready for Agent" no:assignee -is:blockedフロンティアのレシピをミラーリング
6⏳ 入力待ちstatus:"Waiting on input"業務入力が必要なレシピをミラーリング
8📥 トリアージstatus:Backlog no:parent-issue受信箱。トップレベルのみ
9🗺️ エピックtype:Epicエピックのレシピをミラーリング
13🔒 セキュリティlabel:security is:openセキュリティのレシピをミラーリング

ビューの番号は固定されており、削除後に再利用されないため、表の番号には抜けがあります。この表との照合に意味があるのはフィルターだけです。ビューの名前、列、タブの位置は利用者が決めるものであり、そこでの変更はドリフトではなく意図的な変更です。名前を指定する価値がある列は、🗺️ エピックのサブイシューの進捗だけです。これは組み込みフィールドであり、進捗バーはこのビューの存在理由です。

フィルターは書き込み可能です。 updateProjectV2View は name、layout、filter、configuration を受け取るため、ドリフトしたフィルターは報告するだけでなく、修正できます。グループ化と並べ替えは読み取り可能です。ProjectV2View では groupByFields、verticalGroupByFields、sortByFields が公開されていますが、書き込みはできません。ProjectV2ViewConfigurationInput が受け取るのは visibleFieldIds のみです。 両方のボードビューでは、Cycle をスイムレーン、Status を列としてグループ化します。また、アイテムのないイテレーションはスイムレーンとして表示されません。そのため、完了したサイクルはアイテムがアーカイブされるとボードから消え、非表示にするためのフィルターは不要です。

サイクル

サイクルとは、Cycle フィールドのイテレーションです。イテレーションは、それを締めくくるミーティングの前日に終了します。

フィールドのconfigurationから読み取ります。completedIterationsには完了したイテレーションが格納されます。実行中のサイクルは、iterationsのうちstartDateが今日以前で、startDate + durationが今日より後のエントリーです。今後のサイクルは、startDateが今日より後のものです。配列の順序は保証されないため、インデックスではなく日付で選択してください。

Cycleはコミット単位(トップレベルのイシュー、またはエピックの子イシュー)に設定し、その子孫は値を継承します。そのため、Cycleが設定されていないサブイシューは、計画済みであり、割り当て漏れではありません。エピック自体にCycleが設定されることはありません。

常に3つのイテレーションが有効です。実行中のサイクルと今後2つのサイクルがあるため、2回先のミーティングまで延期された作業も、常に割り当て先があります。

タイトルにはテーマを記載します。テーマが決まるまでは、日付範囲を仮のタイトルとして使用します(Sep 29 - Oct 12)。日付はイテレーション自体のstartDateとdurationに格納されます。タイトルは一意でなければなりません。サイクルのレシピはCYCLE_TITLEで照合し、スイムレーンにはタイトルが表示されるため、2つのサイクルのタイトルが同じだと、1つに見えてしまいます。

設定を書き込むとボード全体がクリアされます

updateProjectV2FieldはiterationConfigurationを変更する唯一のミューテーションであり、書き込みのたびに設定全体が置き換えられ、すべてのイテレーションIDが再生成され、すべてのアイテムのCycleがクリアされます。同一の設定を書き戻した場合も同じです。そのため、イテレーションの追加にかかるコストは、名称変更とまったく同じです。また、入力には**completedIterationsがありません**。そのため、iterations内に渡されなかった完了済みサイクルは、完全に削除されます。

サイクルごとに設定の書き込みを1回だけ行い、新しいイテレーション、合意済みの終了日、テーマのタイトルなど、保留中の変更をすべてまとめてから、次の手順で実行してください。

  1. snapshot.graphqlを使って、アーカイブ済みアイテムも含め、すべてのアイテムのCycleをスナップショットします。board.graphqlは現在のアイテムのみを対象とするため、アーカイブ済みアイテムを黙って除外してしまいます。

    gh api graphql --paginate -F query=@"$SNAPSHOT_QUERY" \
      --jq '.data.organization.projectV2.items.nodes[] | select(.cycle.startDate)' \
      > /tmp/cycle-snapshot.jsonl
    
  2. すべてのイテレーションを渡して1回だけ書き込みます。完了済みのイテレーションは過去の日付にしておくと、GitHubがcompletedIterationsに振り分け直します。設定自体のstartDateは読み戻せず、startDayのみが公開されます。そのため、最も早いイテレーションのstartDateを渡してください。

  3. スナップショットのすべての値を復元します。イテレーションは**startDateで**特定してください。すべてのIDが変わっており、タイトルが別の日付に移動している可能性があるためです。アーカイブ済みアイテムは編集できないため、アーカイブを解除し(unarchiveProjectV2Item)、Cycleを設定してから、再度アーカイブします(archiveProjectV2Item)。

  4. completedIterationsと復元件数を読み戻します。completedIterationsが空なら失敗を示しており、件数がスナップショットより少なければ、復元されなかったアイテムがあります。

エピック

エピックとは、1 サイクルでは大きすぎる作業です。言語を参照してください。階層は エピック → issue → サブ issueで、最初と最後の階層は任意です。また、エピック階層は 1 段だけです。エピックは何にも属さず、その子がコミット対象の単位になります。複数サイクルにまたがることが判明した作業は、それぞれ 1 サイクルに収まる子に分割することでエピックになります。

  • Epic issue タイプが目印です。 組織には 3 つのタイプがあります。Bug(何かが壊れている)、Enhancement(それ以外すべて:新機能、および以前は雑務と呼んでいたツール、設定、リファクタリング作業)、そして Epic です。各 issue には必ず 1 つだけタイプを設定します。gh api --method PATCH repos/{owner}/{repo}/issues/{n} -f type=<Bug|Enhancement|Epic>、または REST の作成呼び出しで -f type= を指定して設定します。gh issue create と gh issue edit にはタイプを指定するフラグがありません。GraphQL では issueType { name }、REST では .type.name として読み取れます。gh issue list --search type:Epic または Epics レシピで検索できます。組織にタイプがない場合は、組織ノードの createIssueType で作成します。これには、デフォルトの gh ログインにはない admin:org スコープが必要です。
  • ステータスは通常の段階を進みます。 各子も同様に、ほかの issue と同じようにトリアージします。エピックの分解がまだ必要な間は Backlog、子ができたら Ready for Human、最初の子がサイクルにコミットされたら In progress、担当者がクローズしたら Done です。
  • エピックには Cycle を設定しません。 各子がコミット対象の単位であり、それぞれに独自の Cycle を設定します。そのため、サイクル終了時に未完了の子が残っていれば、その子は未達です。エピックは完了でも未達でもなく、進行中です。
  • 最初の子をコミットする前に分解します。 進捗は直接の子だけを数えるため、すべての子が 1 サイクルで完了する単位である場合にのみ、正確な進捗を示します。サイクルに収まらない子は、階層を 1 段に保ったまま、エピックの子 2 つに分割します。
  • クローズは判断を伴うため、担当者が行います。 候補の検索には Epics ready to close レシピを使います。

作業の延期

延期は、記録する判断です。その形式は、作業をいつ再開するかによって決まります。

  • 将来のサイクル — Cycle を 2 つ先までのいずれかに設定します。issue はオープンのままで、ボードに表示されます。
  • 後日 — 判断、担当者、再開時期をコメントし、--reason "not planned" を指定してクローズします。カレンダーのリマインダーは担当者が設定します。GitHub が代わりに通知することはありません。
  • 時期不明 — トリアージで再検討できるよう、Backlog に残します。

再開するときは登録し直すのではなく再オープンし、スレッドに履歴を残します。

ラベル

ラベルは状態もカテゴリも示しません。状態はStatus、カテゴリはissue タイプです。意味を持つラベルは 2 種類です。

  • wayfinder:* — wayfinder セットが適用します。Wayfinding operationsを参照してください。
  • security — セキュリティの脆弱性、セキュリティ強化、またはセキュリティプロセスの変更に関する issue に適用します。作成時またはトリアージ時に、タイプの設定とあわせて適用してください。gh issue edit <n> --add-label security を使うか、組織フォームから issue を作成するで作成呼び出しに -f 'labels[]=security' を指定します。これはタイプではなくラベルです。issue のタイプは 1 つだけであり、セキュリティに関する指摘は Bug にも Enhancement にもなり得るためです。

bug や enhancement を含むその他のラベルは飾りにすぎません。必要なら参照できますが、これを条件に処理が行われることはありません。

**リポジトリ間で用語を同期する仕組みはありません。**そのため、リポジトリによってはラベルがない場合があります。適用する前に確認し(gh label list --repo <owner>/<repo> --search <name>)、ラベルがなければ、clhbid/agent-context のlabels.ymlに記載された名前、色、説明で作成してください。

gh api repos/clhbid/agent-context/contents/labels.yml --jq .content | base64 -d
gh label create <name> --repo <owner>/<repo> --color <color> --description "<description>"

org フォームから issue を作成する

clhbid/.github/.github/ISSUE_TEMPLATE/ の issue フォームが、issue の本文に含める内容の正本です。

issue を作成する前に、必要なフォームを取得します。

gh api repos/clhbid/.github/contents/.github/ISSUE_TEMPLATE/<bug|enhancement>.yml \
  --jq .content | base64 -d

各フィールドの label を、フォームと同じ順序で issue 本文の ### 見出しとして使用します。writing-lean に従って記述し、必須フィールドをすべて埋めます。該当しない任意フィールドは省略します。このルールは、後から編集する場合を含め、エージェントが作成するすべての本文に適用されます。

フォームのフィールド以外の内容(受け入れ条件、インターフェースに関するメモ、検証手順、フォームにないスコープの境界など)は、本文に見出しを追加するのではなく、issue の agent-brief コメント(または triage-notes コメント)に記載します。afk-loop § The agent brief を参照してください。

REST エンドポイント経由で作成します。これにより、同じ呼び出しでタイプも設定されます。gh issue create には --type フラグがありません。タイプはフォームに対応し、bug.yml には Bug、enhancement.yml には Enhancement を指定します。

gh api repos/<owner>/<repo>/issues -f title="..." -f body="..." -f type=<Bug|Enhancement> \
  --jq .html_url

これで設定されるのはタイプのみで、ステータスは設定されません。その後、issue が Backlog に登録されたことを確認します。Status を参照してください。

テンプレートで設定するのはカテゴリーであり、ステータスではありません

issue フォームは、.github/ISSUE_TEMPLATE/ の clhbid/.github にある組織のデフォルト設定から取得します。issue を作成する人はフォームを使用してください。フォームを使うと、トリアージ担当者が別途確認しなければならない項目を記入できます。

フォームで適用されるのは、カテゴリータイプ(Bug または Enhancement)1 つだけで、ステータスは設定されません。カテゴリーは issue の種類を示し、Status は issue の進捗を示します。カテゴリーからステータスが決まることはありません。 エージェントがブラウザのフォームを使わずに issue を作成する場合は、org フォームから issue を作成するを参照してください。

コミット規約

コミットの件名には、issue 番号をプレフィックスとして単独で付けます(2217: <subject>)。末尾の (#NNNN) は squash-merge で追加される プルリクエスト番号であり、issue 番号ではありません。差分の仕様を確認するときは、先頭の NNNN: を読み、末尾の (#NNNN) は無視してください。

Ready for Agent の前に作業を分割する

1 issue = 1 branch = 1 PR を守ります。作業が大きすぎる場合は、プルリクエストではなく issue を分割します。2 つの issue が同じ変更を扱う場合は、一方をもう一方の重複としてクローズします。サイズは Ready for Agent の前提条件です。サイクル内で完了できない作業はエピックとして、さらに上位の単位で分割します。

小さいほど適切です。 lockfile、スナップショット、生成ファイルを除いた変更行数の上限は約 1000 行ですが、これは目標ではなく上限です。きれいに分割できる変更セットは、それより十分小さく分割してください。レビュアーは小さな差分には目を通し、大きな差分はざっと確認するためです。計画が上限を超える場合は、まず簡素化します。最初に writing-lean の削除テストに通らないドキュメントを削り、その後で分割してください。これをチェックする CI ゲートはありません。

  • 有効な分割単位は、個別にマージでき、チェックがすべて成功するものです。振る舞いに影響しない分割(名前変更、抽出、リファクタリング)も、単独で成立するなら有効です。各分割単位をチェック成功の状態でマージできない計画は Ready for Human に移し、その理由をコメントします。
  • 分割単位が互いに依存する場合は、プルリクエストを積み重ねます。 プルリクエストはデフォルトブランチ(main または development)以外のブランチをターゲットにできます。ベースブランチがマージされると、GitHub が自動的にターゲットを切り替えます。そのため、依存する分割単位も親のマージを待たずにすぐにオープンしてレビューできます。
  • 作業の途中で、エージェントが作業の規模が大きすぎると判断した場合は、作業を簡素化し、サブ issue を作成して子として関連付け、末端の issue を Ready for Agent に設定します。完了した作業については、依存関係に応じて前の分割単位をベースに積み重ねたプルリクエストを作成し、分割の内容を説明するコメントを元の issue に投稿します。

トリアージの対象としてのプルリクエスト

要望を受け付ける窓口としての PR:いいえ。 (このリポジトリで外部からの PR を機能リクエストとして扱う場合は yes に設定します。このフラグは triage が読み取ります。) no の間、外部からの PR はトリアージされず、gh pr のステータス処理も適用されません。

スキルに次のように書かれている場合……

  • 「イシュートラッカーに公開する」 — 組織フォームからイシューを作成するを使って GitHub イシューを作成します。
  • 「関連するチケットを取得する」 — gh issue view <number> --comments を実行します。

Wayfinding の操作

wayfinder で使用します。マップは、チケットとしての子イシューを持つ単一のイシューです。

  • マップ:wayfinder:map ラベルが付いた単一のイシューで、本文に Notes / Decisions-so-far / Fog を記載します。gh issue create --label wayfinder:map を実行します。
  • 子チケット:GitHub のサブイシューとしてマップにリンクされたイシューです。規約を参照してください。ラベル:wayfinder:<type>(research/prototype/grilling/task)。担当者が決まると、チケットは進行を担当する開発者に割り当てられます。
  • ブロック関係:GitHub のネイティブなイシュー依存関係を使います。gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id> で依存関係を追加します。<blocker-db-id> はブロッカーの数値のデータベース IDです(gh api repos/<owner>/<repo>/issues/<n> --jq .id で取得します。#number や node_id ではありません)。GitHub は issue_dependencies_summary.blocked_by(未解決のブロッカーのみ。ライブゲート)を返します。すべてのブロッカーがクローズされると、チケットのブロックが解除されます。
  • フロンティアクエリ:マップの未解決の子イシューを一覧表示し、未解決のブロッカーがあるもの、または担当者がいるものを除外します。マップ上で最初のものが優先されます。
  • 担当する:gh issue edit <n> --add-assignee @me — セッションで最初に行う書き込みです。
  • 解決する:gh issue comment <n> --body "<answer>" を実行し、続けて gh issue close <n> を実行します。その後、コンテキストへのポインター(gist + リンク)をマップの Decisions-so-far に追記します。

注意点

誤ったフィルターでも正しく見えることがあります。 GitHub は、認識できない修飾子があってもエラーを出さずに無視します。どちらの方向にも失敗する可能性があります。誤字があるとすべてが返される一方、イシュー検索で status:"In progress" を指定すると何も返されないことがあります。そのため、結果の件数だけでは何も証明できません。既知の答えが分かっているクエリを使って、新しいフィルターを検証してください。

REST は親子関係について正確な情報を返しません。 gh issue view --json parent はそのままエラーになり、gh api repos/{owner}/{repo}/issues/{n} --jq .parent.number は、子イシューでもトップレベルのイシューと同じく null を返します。GraphQL の Issue.parent は正確な値を返し、board.graphql はこれを選択します。イシューを1件ずつ確認すると毎回リクエストが必要になり、時間に追われていると省略されるため、クエリ自体に no:parent-issue(または .content.parent == null)を含めてください。

アイテムを追加するとアーカイブが解除されます。 すでにアーカイブ済みのアイテムに addProjectV2ItemById を実行すると、エラーを返さずにライブボードへ戻します。アーカイブ済みアイテムはライブアイテムのみを対象とした読み取りでは見えません。そのため、再オープンされたイシューが存在しないように見え、再追加されると、副作用としてアーカイブから戻ってきます。

読み戻した値が古い場合があります。 プロジェクト API は結果整合性を採用しています。書き込みはすべて読み戻して検証しますが、書き込み直後に値が一致しなくても、失敗ではなく未確認として扱ってください。時間をおいて再確認し、一度の読み取り結果の不一致だけを根拠に書き込みを再実行してはいけません。 issueDependenciesSummary.blockedBy にも同じ遅延があります。ブロッカーをリンクした直後は 0 を返し、その少し後に 1 を返したことがあります。そのため、書き込み直後に実行した Agent frontier の手順では、すでにブロックされているイシューが割り当てられる可能性があります。

コメント