4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code のステータスラインに Jira のタスク状態を表示する

4
Posted at

はじめに

Claude Code には、画面下部に任意のシェルスクリプトの出力を表示できる ステータスライン という機能があります。多くの人はモデル名・コンテキスト残量・git ブランチを出す用途で使っていますが、筆者はここに 「いま自分が触っているブランチに対応する Jira 課題のキーとステータス」 を常時表示するようにしました。

一見「Jira の API を叩いて表示するだけ」に見えますが、素朴に実装するとステータスラインが重くなり、体感を確実に悪化させます。本記事では、なぜ直叩きが悪手なのか、そして「ステータスラインは読むだけ・状態はスキルが書く」というキャッシュ経由の設計に落ち着いた経緯を、実際に組んだスクリプトとあわせて共有します。

前提知識

bash と git の基本操作、jq での JSON パース、Jira のような課題管理ツールに「課題キー(例: PROJ-12)」と「ステータス(To Do / In Progress など)」がある、という程度を前提とします。

背景:なぜステータスラインにタスク状態を出したいのか

AI エージェントに実装を任せていると、ターミナルの中身は目まぐるしく流れていきます。そのとき地味に効いてくるのが 「この作業、Jira 上ではどの課題で、いまどのステータスだったか」 を見失う問題です。ブランチ名は見えても、それが Jira のどのチケットに紐づいていて、レビュー中なのか着手前なのかは、いちいち Jira を開かないと分かりません。

そこで「ブランチ → 課題キー → Jira ステータス」を 1 行に畳んで常時表示することにしました。完成イメージはこうです。

[feature/slice-03]  🎫 PROJ-12 report-list|feature: 実装|Jira: In Progress
[fix/slice-03]      🎫 PROJ-31 report-list|fix: 欠陥修正
[spec/slice-04]     🎫 report-edit|spec: 仕様(起票前なのでキーなし)
[main]              🎫 main

ブランチによって表示が変わり、Jira 側で解決できたときだけステータスが付く、という挙動です。

まずステータスラインの「呼ばれ方」を正しく知る

設計判断の前に、ステータスラインがいつ実行されるかを公式ドキュメントで確認しておきます。ここが本記事のキモです。公式ドキュメント には次のように書かれています。

  • スクリプトは アシスタントの新しいメッセージのたび/compact 完了時、パーミッションモード変更時、vim モード切替時に実行される
  • 更新は 300ms でデバウンスされ、実行中に次の更新が来ると 走っている実行はキャンセルされる
  • refreshInterval を設定すると、加えて N 秒ごとにも再実行される

設定は ~/.claude/settings.json に次のように書くだけです。入力は stdin に JSON で渡されます。

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}
#!/bin/bash
input=$(cat)
# 渡ってくる JSON の例: .model.display_name / .workspace.current_dir など
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
echo "📁 ${DIR##*/}"

ここで重要なのは、このスクリプトが「会話のたびに」呼ばれるという事実です。つまりステータスラインは、アプリで言えば描画のたびに走るホットパスに置かれています。

設計判断:ステータスラインは「読むだけ」に徹する

上の事実から、次の結論が出ます。ステータスラインの中で Jira API を直接叩いてはいけません。 理由は 3 つあります。

  1. 遅い。 ネットワーク往復(数百 ms〜秒)をホットパスに入れると、メッセージのたびに待ちが発生します。しかも実行中に次のメッセージが来れば in-flight はキャンセルされるので、そもそも表示が安定しません。
  2. 認証を持てない。 ステータスラインは短命なサブプロセスです。ここに OAuth トークンの取得やリフレッシュを持たせるのは筋が悪く、失敗時のリカバリも書きづらい。
  3. 常駐ポーリングも別の負債。 「バックグラウンドで定期的に Jira を叩いてキャッシュ更新」する常駐プロセスを別に立てる案もありますが、プロセス管理・停止漏れ・レート制限という運用コストを新たに背負うことになります。

そこで採用したのが 「Jira に触る操作をするスキルが、そのタイミングでローカルキャッシュを書き、ステータスラインはそれを読むだけ」 という分離です。図にするとこうなります。

ポイントは、Jira へのネットワークアクセスが「状態が変わる瞬間」だけに限定されることです。着手・レビュー依頼・完了といった遷移は 1 タスクにつき数回しか起きません。その数回のときにだけキャッシュを更新すれば、あとはステータスラインがローカルの JSON を読むだけで済みます。ホットパスからネットワークが消えます。

キャッシュファイルの実体はこんな JSON です(logs/ は gitignore 済み。共有しない前提の派生情報だからです)。

{
  "PROJ-12": { "status": "In Progress", "branch": "feature/slice-03" },
  "PROJ-31": { "status": "In Review",   "branch": "fix/slice-03" }
}

実装1:ブランチ名から課題キーを解決する

まずは「いまのブランチがどの課題キーに対応するか」を求めます。筆者のチームでは作業単位(スライス)ごとにブランチを切り、docs/tickets/registry.md のようなレジストリで「スライス ↔ Jira キー」を対応させています。ブランチの命名は feature/slice-03 fix/slice-03 spec/slice-04 のような規約です。

input=$(cat)
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
cd "$DIR" 2>/dev/null || exit 0

BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[ -z "$BRANCH" ] && exit 0

# ブランチ種別(feature/fix/spec)とスライス名を分解
KIND=${BRANCH%%/*}       # feature
SLICE=${BRANCH#*/}       # slice-03

# レジストリ(Markdown テーブル)からスライス行を引く
# 例: | slice-03 | ... | PROJ-12 | PROJ-31(fix) |
REGISTRY="docs/tickets/registry.md"
ROW=$(grep -F "| $SLICE " "$REGISTRY" 2>/dev/null | head -1)

# パイプ区切りから Jira キー列を取り出す(列位置は運用で固定)
KEY=$(echo "$ROW" | awk -F'|' '{gsub(/ /,"",$4); print $4}')

# fix ブランチなら (fix) 併記キーを優先
if [ "$KIND" = "fix" ]; then
  FIXKEY=$(echo "$ROW" | grep -oE '[A-Z]+-[0-9]+\(fix\)' | grep -oE '[A-Z]+-[0-9]+')
  [ -n "$FIXKEY" ] && KEY=$FIXKEY
fi

列位置(上記では $4)はレジストリの書式に依存するので、各自の課題キーの持ち方に合わせて調整してください。要は 「ブランチ名という手元の事実」を起点にキーを一意に決める、という一点が大事です。

実装2:キャッシュと突き合わせ、「誤表示より非表示」で出す

キーが決まったら、キャッシュファイルからステータスを引きます。ここで筆者が徹底しているのが 「誤表示より非表示」 の原則です。

CACHE="logs/state/jira-status.json"
KEY="PROJ-12"   # 実装1 で解決したキー

STATUS=""
if [ -f "$CACHE" ] && [ -n "$KEY" ]; then
  # キャッシュに載っていて、かつ記録時のブランチが現在のブランチと一致するときだけ採用
  CACHED_BRANCH=$(jq -r --arg k "$KEY" '.[$k].branch // empty' "$CACHE")
  if [ "$CACHED_BRANCH" = "$BRANCH" ]; then
    STATUS=$(jq -r --arg k "$KEY" '.[$k].status // empty' "$CACHE")
  fi
fi

# 表示を組み立てる。キーやステータスが取れなければ、その部分は黙って省く
LINE="[$BRANCH]  🎫 ${KEY:-$BRANCH}"
[ -n "$STATUS" ] && LINE="$LINE|Jira: $STATUS"
echo "$LINE"

「キャッシュに記録されたブランチが現在のブランチと一致するときだけステータスを出す」というガードが効いています。これがないと、別のブランチで着手したときの古いステータスが残って 平然と嘘を表示 します。ステータスラインは常に目に入るので、嘘を出すくらいならその項目ごと消えたほうが害が少ない、という判断です。キャッシュが無ければ無いなりに、キーだけ・ブランチ名だけで縮退して表示します。

この「取れなければ黙って省く(exit 0 も含めて絶対にエラーで落ちない)」書き方は、ステータスライン全般で有効な作法です。落ちると行ごと消えるので、常に成功終了させて部分表示に倒します。

実装3:状態遷移スキルがキャッシュを書く

肝心の「誰がキャッシュを書くか」ですが、これは Jira に状態変化を起こすスキル自身 です。筆者の環境では次のように役割を割り当てています。

  • /pickup(着手)→ Jira を In Progress に更新し、自分を assignee に設定
  • /submit(レビュー依頼)→ In Review に更新
  • /integrate(統合完了)→ Done に更新

これらのスキルは、もともと MCP 経由で Jira を更新します。その更新に成功した直後に、同じ値を logs/state/jira-status.json にも書き込むだけです。ステータスラインは一切 Jira を知りません。「状態を変えた本人が、変えたついでにローカルにも記録する」 という素直な流れになります。

なぜ CI(GitHub Actions など)でやらないのか、とも聞かれます。理由は、下流ブランチは push しないワークフローなので、push を起点にする CI トリガーでは「作業中」の遷移を拾えない からです。着手やレビュー依頼は push を伴わないことが多く、状態遷移の実行主体は「操作したスキル」に置くのが自然でした。

個人判定は git の user.email + メンバー表で

/pickup で「自分を assignee にする」には、手元の作業者が Jira 上の誰か を判定する必要があります。ここで GitHub のマルチアカウントに手を出すと運用が一気に複雑になるので、筆者は次のシンプルな方式にしました。

  • 判定の拠り所は git config user.email(手元の事実)
  • それをリポジトリ内の メンバー表docs/tickets/members.mdemail / Jira accountId / 表示名 の 3 列)で引く
EMAIL=$(git config user.email)
# members.md から email 行を引いて Jira accountId を得る(初回は MCP で解決して追記)

「手元の git アイデンティティ」と「リポジトリにコミットされたメンバー表」だけで個人が決まるので、追加のアカウント連携が要りません。表示名は KPI 集計とも共通化しておくと、名寄せが一箇所で済みます。

ハマりどころ

呼ばれてもいないのに無表示になる。 最初、既存のステータスラインが「存在しない集計スクリプトを呼ぶ」設定のままで、スクリプトが無いと silent exit して何も出ない状態でした。ステータスラインは黙って落ちる作りにしているぶん、無表示のときは「そもそも設定が正しく走っているか」を最初に疑うべきです。bash ~/.claude/statusline.sh < サンプルJSON で手動実行して切り分けます。

古いステータスが残って嘘をつく。 実装2 のブランチ一致ガードが無かった頃、別ブランチのステータスが残留しました。派生情報(キャッシュ)は必ず「どの事実に基づくか(=どのブランチで記録したか)」を一緒に持たせ、現在の事実と一致するときだけ採用する、が鉄則です。

idle 中に git 状態が変わっても更新されない。 バックグラウンドのサブエージェントが git を触っても、メインセッションが idle だとステータスラインの更新トリガーは飛びません。時間依存や外部要因で変わる表示を持つなら、refreshInterval を併用します(公式の該当箇所)。

まとめ

ステータスラインに外部サービスの状態を出したくなったら、まず 「このスクリプトは会話のたびに走るホットパスだ」 を思い出してください。そこから自然に、

  • 表示(読む)と更新(書く)を分離する
  • 更新は「状態が変わる瞬間」に、それを起こした主体が行う
  • 表示側はローカルの派生情報を読むだけで、取れなければ黙って省く(誤表示より非表示)

という設計に落ちます。Jira に限らず、CI の結果でもデプロイ状態でも、「ホットパスからネットワークを追い出し、キャッシュ経由にする」 という考え方はそのまま応用できます。

参考

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?